Compare commits
58 Commits
cli@1.34.1
...
storage@0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd04e5442d | ||
|
|
a5fb73954b | ||
|
|
9ac74fc0bb | ||
|
|
c27dbdc8aa | ||
|
|
bc14fb67a7 | ||
|
|
a68d261d8e | ||
|
|
fbbda53643 | ||
|
|
42db1e59ac | ||
|
|
c866650281 | ||
|
|
fe40d1f4f5 | ||
|
|
8bed2393c3 | ||
|
|
d150463b0b | ||
|
|
55bda3f56b | ||
|
|
b97e2b4d92 | ||
|
|
2311e1dd77 | ||
|
|
824ee142c4 | ||
|
|
c662d063a7 | ||
|
|
b518132349 | ||
|
|
b677d3768f | ||
|
|
51ec151752 | ||
|
|
223322d654 | ||
|
|
add2c20c95 | ||
|
|
961bc5feea | ||
|
|
0ca89974b9 | ||
|
|
e8d52859a3 | ||
|
|
67740ebe3d | ||
|
|
d6f7b01aee | ||
|
|
0fc65df78d | ||
|
|
52e3db7f61 | ||
|
|
235449d68c | ||
|
|
323834d212 | ||
|
|
f7bd250f73 | ||
|
|
579f9dbf31 | ||
|
|
9f2b93d44b | ||
|
|
1aeef26ec6 | ||
|
|
749bb4e637 | ||
|
|
accabc83f7 | ||
|
|
8c127d7b6b | ||
|
|
f9c614ef99 | ||
|
|
1d183f7fc4 | ||
|
|
46e740f060 | ||
|
|
0d30ab4eec | ||
|
|
d5fd3cb59c | ||
|
|
f36d360b9e | ||
|
|
61af5087fd | ||
|
|
7429d8ae3f | ||
|
|
8ce9705b17 | ||
|
|
5b53c568ad | ||
|
|
24c5db943d | ||
|
|
ea87b81db6 | ||
|
|
226a22e322 | ||
|
|
9c58b4307a | ||
|
|
7ecfa41790 | ||
|
|
2633747992 | ||
|
|
3b107a386e | ||
|
|
b5ed48a832 | ||
|
|
363730ab20 | ||
|
|
9c77c4be51 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,6 +7,8 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
> **Note:** Bug reports that are clearly AI-generated will not be accepted and will be closed immediately. Please write your bug report in your own words.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -7,6 +7,8 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
> **Note:** Feature requests that are clearly AI-generated will not be accepted and will be closed immediately. Please write your feature request in your own words.
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
|
||||
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -8,6 +8,8 @@
|
||||
|
||||
--- Delete everything below this line before submitting your PR ---
|
||||
|
||||
> **Note on AI-assisted contributions:** Contributions with the help of AI are permitted, but you are ultimately responsible for the quality of your submission and for ensuring it follows our contributing guidelines. **The PR description must be written in your own words and be clear and concise**. Please ensure you remove any superfluous code comments introduced by AI tools before submitting. PRs that clearly violate this rule will be closed without further review.
|
||||
|
||||
### PR title format
|
||||
|
||||
The PR title must follow the following pattern:
|
||||
@@ -30,6 +32,7 @@ Where `PKG` is:
|
||||
- `deps`: For changes to dependencies
|
||||
- `docs`: For changes to the documentation
|
||||
- `examples`: For changes to the examples
|
||||
- `internal/lib`: For changes to Nhost's common libraries (internal)
|
||||
- `mintlify-openapi`: For changes to the Mintlify OpenAPI tool
|
||||
- `nhost-js`: For changes to the Nhost JavaScript SDK
|
||||
- `nixops`: For changes to the NixOps
|
||||
|
||||
@@ -17,7 +17,7 @@ runs:
|
||||
|
||||
# Define valid types and packages
|
||||
VALID_TYPES="feat|fix|chore"
|
||||
VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|mintlify-openapi|nhost-js|nixops|storage"
|
||||
VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|internal\/lib|mintlify-openapi|nhost-js|nixops|storage"
|
||||
|
||||
# Check if title matches the pattern TYPE(PKG): SUMMARY
|
||||
if [[ ! "$PR_TITLE" =~ ^(${VALID_TYPES})\((${VALID_PKGS})\):\ .+ ]]; then
|
||||
|
||||
8
.github/workflows/auth_checks.yaml
vendored
8
.github/workflows/auth_checks.yaml
vendored
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "auth: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/auth_checks.yaml'
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
@@ -18,6 +17,7 @@ on:
|
||||
- '.golangci.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'internal/lib/**'
|
||||
- 'vendor/**'
|
||||
|
||||
# auth
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
with:
|
||||
NAME: auth
|
||||
PATH: services/auth
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
with:
|
||||
NAME: auth
|
||||
PATH: services/auth
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: true
|
||||
secrets:
|
||||
|
||||
9
.github/workflows/cli_checks.yaml
vendored
9
.github/workflows/cli_checks.yaml
vendored
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "cli: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/cli_checks.yaml'
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
@@ -50,7 +49,7 @@ jobs:
|
||||
with:
|
||||
NAME: cli
|
||||
PATH: cli
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -65,7 +64,7 @@ jobs:
|
||||
with:
|
||||
NAME: cli
|
||||
PATH: cli
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: true
|
||||
secrets:
|
||||
@@ -81,7 +80,7 @@ jobs:
|
||||
with:
|
||||
NAME: cli
|
||||
PATH: cli
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: "Get artifacts"
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
path: ~/artifacts
|
||||
|
||||
|
||||
7
.github/workflows/codegen_checks.yaml
vendored
7
.github/workflows/codegen_checks.yaml
vendored
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "codegen: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/codegen_checks.yaml'
|
||||
@@ -48,7 +47,7 @@ jobs:
|
||||
with:
|
||||
NAME: codegen
|
||||
PATH: tools/codegen
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -62,7 +61,7 @@ jobs:
|
||||
with:
|
||||
NAME: codegen
|
||||
PATH: tools/codegen
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
secrets:
|
||||
|
||||
14
.github/workflows/dashboard_checks.yaml
vendored
14
.github/workflows/dashboard_checks.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: "dashboard: check and build"
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_build_artifacts.yaml'
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: dashboard
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
ENVIRONMENT: preview
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
with:
|
||||
NAME: dashboard
|
||||
PATH: dashboard
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: true
|
||||
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
with:
|
||||
NAME: dashboard
|
||||
PATH: dashboard
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
with:
|
||||
NAME: dashboard
|
||||
PATH: dashboard
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
NHOST_TEST_DASHBOARD_URL: ${{ needs.deploy-vercel.outputs.preview-url }}
|
||||
NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }}
|
||||
NHOST_TEST_ORGANIZATION_NAME: ${{ vars.NHOST_TEST_ORGANIZATION_NAME }}
|
||||
@@ -126,8 +126,10 @@ jobs:
|
||||
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
|
||||
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET: ${{ secrets.NHOST_TEST_PROJECT_ADMIN_SECRET }}
|
||||
NHOST_TEST_FREE_USER_EMAILS: ${{ secrets.NHOST_TEST_FREE_USER_EMAILS }}
|
||||
NHOST_TEST_ONBOARDING_USER: ${{ secrets.NHOST_TEST_ONBOARDING_USER }}
|
||||
PLAYWRIGHT_REPORT_ENCRYPTION_KEY: ${{ secrets.PLAYWRIGHT_REPORT_ENCRYPTION_KEY }}
|
||||
NHOST_TEST_STAGING_SUBDOMAIN: ${{ secrets.NHOST_TEST_STAGING_SUBDOMAIN }}
|
||||
NHOST_TEST_STAGING_REGION: ${{ secrets.NHOST_TEST_STAGING_REGION }}
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
15
.github/workflows/dashboard_wf_e2e_staging.yaml
vendored
15
.github/workflows/dashboard_wf_e2e_staging.yaml
vendored
@@ -52,12 +52,16 @@ on:
|
||||
required: true
|
||||
NHOST_TEST_USER_PASSWORD:
|
||||
required: true
|
||||
NHOST_TEST_ONBOARDING_USER:
|
||||
required: true
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET:
|
||||
required: true
|
||||
NHOST_TEST_FREE_USER_EMAILS:
|
||||
required: true
|
||||
PLAYWRIGHT_REPORT_ENCRYPTION_KEY:
|
||||
required: true
|
||||
NHOST_TEST_STAGING_SUBDOMAIN:
|
||||
required: true
|
||||
NHOST_TEST_STAGING_REGION:
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: dashboard-e2e-staging
|
||||
@@ -77,7 +81,10 @@ env:
|
||||
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
|
||||
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET: ${{ secrets.NHOST_TEST_PROJECT_ADMIN_SECRET }}
|
||||
NHOST_TEST_FREE_USER_EMAILS: ${{ secrets.NHOST_TEST_FREE_USER_EMAILS }}
|
||||
NHOST_TEST_ONBOARDING_USER: ${{ secrets.NHOST_TEST_ONBOARDING_USER }}
|
||||
NHOST_TEST_STAGING_SUBDOMAIN: ${{ secrets.NHOST_TEST_STAGING_SUBDOMAIN }}
|
||||
NHOST_TEST_STAGING_REGION: ${{ secrets.NHOST_TEST_STAGING_REGION }}
|
||||
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
@@ -141,7 +148,7 @@ jobs:
|
||||
rm playwright-report.tar.gz
|
||||
|
||||
- name: Upload encrypted Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
if: failure()
|
||||
with:
|
||||
name: encrypted-playwright-report-${{ github.run_id }}
|
||||
|
||||
4
.github/workflows/docs_checks.yaml
vendored
4
.github/workflows/docs_checks.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: "docs: check and build"
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/dashboard_checks.yaml'
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
with:
|
||||
NAME: docs
|
||||
PATH: docs
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
|
||||
7
.github/workflows/examples_demos_checks.yaml
vendored
7
.github/workflows/examples_demos_checks.yaml
vendored
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "examples/demos: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/examples_demos_checks.yaml'
|
||||
@@ -64,7 +63,7 @@ jobs:
|
||||
with:
|
||||
NAME: demos
|
||||
PATH: examples/demos
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -78,7 +77,7 @@ jobs:
|
||||
with:
|
||||
NAME: demos
|
||||
PATH: examples/demos
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "examples/guides: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/examples_guides_checks.yaml'
|
||||
@@ -64,7 +63,7 @@ jobs:
|
||||
with:
|
||||
NAME: guides
|
||||
PATH: examples/guides
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -78,7 +77,7 @@ jobs:
|
||||
with:
|
||||
NAME: guides
|
||||
PATH: examples/guides
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "examples/tutorials: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/examples_tutorials_checks.yaml'
|
||||
@@ -64,7 +63,7 @@ jobs:
|
||||
with:
|
||||
NAME: tutorials
|
||||
PATH: examples/tutorials
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -78,7 +77,7 @@ jobs:
|
||||
with:
|
||||
NAME: tutorials
|
||||
PATH: examples/tutorials
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
OS_MATRIX: '["blacksmith-2vcpu-ubuntu-2404"]'
|
||||
|
||||
3
.github/workflows/gen_ai_review.yaml
vendored
3
.github/workflows/gen_ai_review.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: "gen: AI review"
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
@@ -24,4 +24,5 @@ jobs:
|
||||
config.model: ${{ vars.GEN_AI_MODEL }}
|
||||
config.model_turbo: $${{ vars.GEN_AI_MODEL_TURBO }}
|
||||
config.max_model_tokens: 200000
|
||||
config.custom_model_max_tokens: 200000
|
||||
ignore.glob: "['pnpm-lock.yaml','**/pnpm-lock.yaml', 'vendor/**','**/client_gen.go','**/models_gen.go','**/generated.go','**/*.gen.go']"
|
||||
|
||||
7
.github/workflows/nhost-js_checks.yaml
vendored
7
.github/workflows/nhost-js_checks.yaml
vendored
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "nhost-js: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/nhost-js_checks.yaml'
|
||||
@@ -65,7 +64,7 @@ jobs:
|
||||
with:
|
||||
NAME: nhost-js
|
||||
PATH: packages/nhost-js
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -79,7 +78,7 @@ jobs:
|
||||
with:
|
||||
NAME: nhost-js
|
||||
PATH: packages/nhost-js
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
secrets:
|
||||
|
||||
7
.github/workflows/nixops_checks.yaml
vendored
7
.github/workflows/nixops_checks.yaml
vendored
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "nixops: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/nixops_checks.yaml'
|
||||
@@ -40,7 +39,7 @@ jobs:
|
||||
with:
|
||||
NAME: nixops
|
||||
PATH: nixops
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -54,7 +53,7 @@ jobs:
|
||||
with:
|
||||
NAME: nixops
|
||||
PATH: nixops
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: true
|
||||
secrets:
|
||||
|
||||
8
.github/workflows/storage_checks.yaml
vendored
8
.github/workflows/storage_checks.yaml
vendored
@@ -1,8 +1,7 @@
|
||||
---
|
||||
name: "storage: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '.github/workflows/storage_checks.yaml'
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
@@ -18,6 +17,7 @@ on:
|
||||
- '.golangci.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'internal/lib/**'
|
||||
- 'vendor/**'
|
||||
|
||||
# storage
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
with:
|
||||
NAME: storage
|
||||
PATH: services/storage
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
with:
|
||||
NAME: storage
|
||||
PATH: services/storage
|
||||
GIT_REF: ${{ github.sha }}
|
||||
GIT_REF: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: true
|
||||
secrets:
|
||||
|
||||
4
.github/workflows/wf_build_artifacts.yaml
vendored
4
.github/workflows/wf_build_artifacts.yaml
vendored
@@ -85,7 +85,7 @@ jobs:
|
||||
zip -r result.zip result
|
||||
|
||||
- name: "Push artifact to artifact repository"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ inputs.NAME }}-artifact-${{ steps.vars.outputs.ARCH }}-${{ steps.vars.outputs.VERSION }}
|
||||
path: ${{ inputs.PATH }}/result.zip
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
if: ${{ ( inputs.DOCKER ) }}
|
||||
|
||||
- name: "Push docker image to artifact repository"
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ inputs.NAME }}-docker-image-${{ steps.vars.outputs.ARCH }}-${{ steps.vars.outputs.VERSION }}
|
||||
path: ${{ inputs.PATH }}/result
|
||||
|
||||
2
.github/workflows/wf_docker_push_image.yaml
vendored
2
.github/workflows/wf_docker_push_image.yaml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
echo "VERSION=$(make get-version VER=${{ inputs.VERSION }})" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Get artifacts"
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
path: ~/artifacts
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
echo "VERSION=$(make get-version VER=${{ inputs.VERSION }})" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Get artifacts"
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
path: ~/artifacts
|
||||
|
||||
|
||||
@@ -16,6 +16,15 @@ Contributions are made to Nhost repos via Issues and Pull Requests (PRs). A few
|
||||
- We work hard to make sure issues are handled on time, but it could take a while to investigate the root cause depending on the impact. A friendly ping in the comment thread to the submitter or a contributor can help draw attention if your issue is blocking.
|
||||
- If you've never contributed before, see [the first-timer's guide](https://github.com/firstcontributions/first-contributions) for resources and tips on getting started.
|
||||
|
||||
### AI-Assisted Contributions
|
||||
|
||||
We have specific policies regarding AI-assisted contributions:
|
||||
|
||||
- **Issues**: Bug reports and feature requests that are clearly AI-generated will not be accepted and will be closed immediately. Please write your issues in your own words to ensure they are clear, specific, and contain the necessary context.
|
||||
- **Pull Requests**: Contributions with the help of AI are permitted, but you are ultimately responsible for the quality of your submission and for ensuring it follows our contributing guidelines. The PR description must be written in your own words. Additionally, please remove any superfluous code comments introduced by AI tools before submitting. PRs that clearly violate this rule will be closed without further review.
|
||||
|
||||
In all cases, contributors must ensure their submissions are thoughtful, well-tested, and meet the project's quality standards.
|
||||
|
||||
### Issues
|
||||
|
||||
Issues should be used to report problems with Nhost, request a new feature, or discuss potential changes before a PR is created.
|
||||
|
||||
@@ -2,5 +2,8 @@
|
||||
// $schema provides code completion hints to IDEs.
|
||||
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
|
||||
"moderate": true,
|
||||
"allowlist": ["vue-template-compiler", { "id": "CVE-2025-48068", "path": "next" }]
|
||||
"allowlist": [
|
||||
"GHSA-9965-vmph-33xx", // https://github.com/advisories/GHSA-9965-vmph-33xx Update package once have a fix
|
||||
"GHSA-7mvr-c777-76hp" // https://github.com/advisories/GHSA-7mvr-c777-76hp Update package once Nix side is also updated
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,26 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [cli@1.34.4] - 2025-10-28
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(cli)* Update NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL correctly (#3643)
|
||||
|
||||
## [cli@1.34.3] - 2025-10-27
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(cli)* Update schema (#3622)
|
||||
- *(cli)* Bump nhost/dashboard to 2.40.0 (#3629)
|
||||
|
||||
## [cli@1.34.2] - 2025-10-20
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(cli)* Minor fix to download script when specifying version (#3602)
|
||||
- *(cli)* Update schema (#3613)
|
||||
|
||||
## [cli@1.34.1] - 2025-10-13
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
@@ -56,7 +56,7 @@ func CommandCloud() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagDashboardVersion,
|
||||
Usage: "Dashboard version to use",
|
||||
Value: "nhost/dashboard:2.38.4",
|
||||
Value: "nhost/dashboard:2.40.0",
|
||||
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
|
||||
@@ -111,7 +111,7 @@ func CommandUp() *cli.Command { //nolint:funlen
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagDashboardVersion,
|
||||
Usage: "Dashboard version to use",
|
||||
Value: "nhost/dashboard:2.38.4",
|
||||
Value: "nhost/dashboard:2.40.0",
|
||||
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
|
||||
@@ -56,6 +56,7 @@ func auth( //nolint:funlen
|
||||
false,
|
||||
false,
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
"5181f67e2844e4b60d571fa346cac9c37fc00d1ff519212eae6cead138e639ba",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get hasura env vars: %w", err)
|
||||
|
||||
@@ -33,6 +33,7 @@ func expectedAuth() *Service {
|
||||
"AUTH_DISABLE_SIGNUP": "false",
|
||||
"AUTH_EMAIL_PASSWORDLESS_ENABLED": "true",
|
||||
"AUTH_EMAIL_SIGNIN_EMAIL_VERIFIED_REQUIRED": "true",
|
||||
"AUTH_ENCRYPTION_KEY": "5181f67e2844e4b60d571fa346cac9c37fc00d1ff519212eae6cead138e639ba",
|
||||
"AUTH_GRAVATAR_DEFAULT": "gravatarDefault",
|
||||
"AUTH_GRAVATAR_ENABLED": "true",
|
||||
"AUTH_GRAVATAR_RATING": "gravatarRating",
|
||||
|
||||
@@ -344,7 +344,7 @@ func dashboard(
|
||||
subdomain, "hasura", httpPort, useTLS,
|
||||
) + "/console",
|
||||
"NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL": URL(
|
||||
subdomain, "hasura", httpPort, useTLS),
|
||||
subdomain, "hasura", httpPort, useTLS) + "/apis/migrate",
|
||||
"NEXT_PUBLIC_NHOST_STORAGE_URL": URL(
|
||||
subdomain, "storage", httpPort, useTLS) + "/v1",
|
||||
},
|
||||
|
||||
@@ -44,7 +44,7 @@ if [[ "$version" == "latest" ]]; then
|
||||
release=$(curl --silent https://api.github.com/repos/nhost/nhost/releases\?per_page=100 | grep tag_name | grep \"cli\@ | head -n 1 | sed 's/.*"tag_name": "\([^"]*\)".*/\1/')
|
||||
version=$( echo $release | sed 's/.*@//')
|
||||
else
|
||||
release="cli@$release"
|
||||
release="cli@$version"
|
||||
fi
|
||||
|
||||
# check version exists
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package auth provides primitives to interact with the openapi HTTP API.
|
||||
//
|
||||
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.4.1 DO NOT EDIT.
|
||||
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.5.0 DO NOT EDIT.
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package graphql provides primitives to interact with the openapi HTTP API.
|
||||
//
|
||||
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.4.1 DO NOT EDIT.
|
||||
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version 2.5.0 DO NOT EDIT.
|
||||
package graphql
|
||||
|
||||
import (
|
||||
|
||||
@@ -148,7 +148,7 @@ import (
|
||||
#Hasura: {
|
||||
// Version of hasura, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/hasura/graphql-engine/tags
|
||||
version: string | *"v2.46.0-ce"
|
||||
version: string | *"v2.48.5-ce"
|
||||
|
||||
// JWT Secrets configuration
|
||||
jwtSecrets: [#JWTSecret]
|
||||
@@ -223,7 +223,7 @@ import (
|
||||
// Releases:
|
||||
//
|
||||
// https://github.com/nhost/hasura-storage/releases
|
||||
version: string | *"0.7.2"
|
||||
version: string | *"0.8.2"
|
||||
|
||||
// Networking (custom domains at the moment) are not allowed as we need to do further
|
||||
// configurations in the CDN. We will enable it again in the future.
|
||||
@@ -311,7 +311,7 @@ import (
|
||||
// Releases:
|
||||
//
|
||||
// https://github.com/nhost/hasura-auth/releases
|
||||
version: string | *"0.38.1"
|
||||
version: string | *"0.42.4"
|
||||
|
||||
// Resources for the service
|
||||
resources?: #Resources
|
||||
@@ -651,6 +651,9 @@ import (
|
||||
iops: uint32 | *3000
|
||||
tput: uint32 | *125
|
||||
}
|
||||
|
||||
encryptColumnKey?: string & =~"^[0-9a-fA-F]{64}$" // 32 bytes hex-encoded key
|
||||
oldEncryptColumnKey?: string & =~"^[0-9a-fA-F]{64}$" // for key rotation
|
||||
}
|
||||
|
||||
persistentVolumesEncrypted: bool | *false
|
||||
|
||||
@@ -70,18 +70,28 @@ type ConfigAIUpdateInput struct {
|
||||
WebhookSecret *string `json:"webhookSecret,omitempty"`
|
||||
}
|
||||
|
||||
// Configuration for auth service
|
||||
// You can find more information about the configuration here:
|
||||
// https://github.com/nhost/hasura-auth/blob/main/docs/environment-variables.md
|
||||
type ConfigAuth struct {
|
||||
ElevatedPrivileges *ConfigAuthElevatedPrivileges `json:"elevatedPrivileges,omitempty"`
|
||||
Method *ConfigAuthMethod `json:"method,omitempty"`
|
||||
Misc *ConfigAuthMisc `json:"misc,omitempty"`
|
||||
RateLimit *ConfigAuthRateLimit `json:"rateLimit,omitempty"`
|
||||
Redirections *ConfigAuthRedirections `json:"redirections,omitempty"`
|
||||
Resources *ConfigResources `json:"resources,omitempty"`
|
||||
Session *ConfigAuthSession `json:"session,omitempty"`
|
||||
SignUp *ConfigAuthSignUp `json:"signUp,omitempty"`
|
||||
Totp *ConfigAuthTotp `json:"totp,omitempty"`
|
||||
User *ConfigAuthUser `json:"user,omitempty"`
|
||||
Version *string `json:"version,omitempty"`
|
||||
// Resources for the service
|
||||
Resources *ConfigResources `json:"resources,omitempty"`
|
||||
Session *ConfigAuthSession `json:"session,omitempty"`
|
||||
SignUp *ConfigAuthSignUp `json:"signUp,omitempty"`
|
||||
Totp *ConfigAuthTotp `json:"totp,omitempty"`
|
||||
User *ConfigAuthUser `json:"user,omitempty"`
|
||||
// Version of auth, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/nhost/hasura-auth/tags
|
||||
//
|
||||
// Releases:
|
||||
//
|
||||
// https://github.com/nhost/hasura-auth/releases
|
||||
Version *string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigAuthElevatedPrivileges struct {
|
||||
@@ -111,9 +121,11 @@ type ConfigAuthMethodAnonymousUpdateInput struct {
|
||||
}
|
||||
|
||||
type ConfigAuthMethodEmailPassword struct {
|
||||
EmailVerificationRequired *bool `json:"emailVerificationRequired,omitempty"`
|
||||
HibpEnabled *bool `json:"hibpEnabled,omitempty"`
|
||||
PasswordMinLength *uint32 `json:"passwordMinLength,omitempty"`
|
||||
EmailVerificationRequired *bool `json:"emailVerificationRequired,omitempty"`
|
||||
// Disabling email+password sign in is not implmented yet
|
||||
// enabled: bool | *true
|
||||
HibpEnabled *bool `json:"hibpEnabled,omitempty"`
|
||||
PasswordMinLength *uint32 `json:"passwordMinLength,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigAuthMethodEmailPasswordUpdateInput struct {
|
||||
@@ -335,8 +347,10 @@ type ConfigAuthRateLimitUpdateInput struct {
|
||||
}
|
||||
|
||||
type ConfigAuthRedirections struct {
|
||||
// AUTH_ACCESS_CONTROL_ALLOWED_REDIRECT_URLS
|
||||
AllowedUrls []string `json:"allowedUrls,omitempty"`
|
||||
ClientURL *string `json:"clientUrl,omitempty"`
|
||||
// AUTH_CLIENT_URL
|
||||
ClientURL *string `json:"clientUrl,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigAuthRedirectionsUpdateInput struct {
|
||||
@@ -350,8 +364,10 @@ type ConfigAuthSession struct {
|
||||
}
|
||||
|
||||
type ConfigAuthSessionAccessToken struct {
|
||||
// AUTH_JWT_CUSTOM_CLAIMS
|
||||
CustomClaims []*ConfigAuthsessionaccessTokenCustomClaims `json:"customClaims,omitempty"`
|
||||
ExpiresIn *uint32 `json:"expiresIn,omitempty"`
|
||||
// AUTH_ACCESS_TOKEN_EXPIRES_IN
|
||||
ExpiresIn *uint32 `json:"expiresIn,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigAuthSessionAccessTokenUpdateInput struct {
|
||||
@@ -360,6 +376,7 @@ type ConfigAuthSessionAccessTokenUpdateInput struct {
|
||||
}
|
||||
|
||||
type ConfigAuthSessionRefreshToken struct {
|
||||
// AUTH_REFRESH_TOKEN_EXPIRES_IN
|
||||
ExpiresIn *uint32 `json:"expiresIn,omitempty"`
|
||||
}
|
||||
|
||||
@@ -373,9 +390,11 @@ type ConfigAuthSessionUpdateInput struct {
|
||||
}
|
||||
|
||||
type ConfigAuthSignUp struct {
|
||||
DisableNewUsers *bool `json:"disableNewUsers,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Turnstile *ConfigAuthSignUpTurnstile `json:"turnstile,omitempty"`
|
||||
// AUTH_DISABLE_NEW_USERS
|
||||
DisableNewUsers *bool `json:"disableNewUsers,omitempty"`
|
||||
// Inverse of AUTH_DISABLE_SIGNUP
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Turnstile *ConfigAuthSignUpTurnstile `json:"turnstile,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigAuthSignUpTurnstile struct {
|
||||
@@ -425,12 +444,16 @@ type ConfigAuthUser struct {
|
||||
}
|
||||
|
||||
type ConfigAuthUserEmail struct {
|
||||
// AUTH_ACCESS_CONTROL_ALLOWED_EMAILS
|
||||
Allowed []string `json:"allowed,omitempty"`
|
||||
// AUTH_ACCESS_CONTROL_BLOCKED_EMAILS
|
||||
Blocked []string `json:"blocked,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigAuthUserEmailDomains struct {
|
||||
// AUTH_ACCESS_CONTROL_ALLOWED_EMAIL_DOMAINS
|
||||
Allowed []string `json:"allowed,omitempty"`
|
||||
// AUTH_ACCESS_CONTROL_BLOCKED_EMAIL_DOMAINS
|
||||
Blocked []string `json:"blocked,omitempty"`
|
||||
}
|
||||
|
||||
@@ -446,6 +469,7 @@ type ConfigAuthUserEmailUpdateInput struct {
|
||||
|
||||
type ConfigAuthUserGravatar struct {
|
||||
Default *string `json:"default,omitempty"`
|
||||
// AUTH_GRAVATAR_ENABLED
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Rating *string `json:"rating,omitempty"`
|
||||
}
|
||||
@@ -457,8 +481,10 @@ type ConfigAuthUserGravatarUpdateInput struct {
|
||||
}
|
||||
|
||||
type ConfigAuthUserLocale struct {
|
||||
// AUTH_LOCALE_ALLOWED_LOCALES
|
||||
Allowed []string `json:"allowed,omitempty"`
|
||||
Default *string `json:"default,omitempty"`
|
||||
// AUTH_LOCALE_DEFAULT
|
||||
Default *string `json:"default,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigAuthUserLocaleUpdateInput struct {
|
||||
@@ -467,8 +493,10 @@ type ConfigAuthUserLocaleUpdateInput struct {
|
||||
}
|
||||
|
||||
type ConfigAuthUserRoles struct {
|
||||
// AUTH_USER_DEFAULT_ALLOWED_ROLES
|
||||
Allowed []string `json:"allowed,omitempty"`
|
||||
Default *string `json:"default,omitempty"`
|
||||
// AUTH_USER_DEFAULT_ROLE
|
||||
Default *string `json:"default,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigAuthUserRolesUpdateInput struct {
|
||||
@@ -484,6 +512,7 @@ type ConfigAuthUserUpdateInput struct {
|
||||
Roles *ConfigAuthUserRolesUpdateInput `json:"roles,omitempty"`
|
||||
}
|
||||
|
||||
// AUTH_JWT_CUSTOM_CLAIMS
|
||||
type ConfigAuthsessionaccessTokenCustomClaims struct {
|
||||
Default *string `json:"default,omitempty"`
|
||||
Key string `json:"key"`
|
||||
@@ -522,8 +551,11 @@ type ConfigClaimMapUpdateInput struct {
|
||||
Value *string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// Resource configuration for a service
|
||||
type ConfigComputeResources struct {
|
||||
CPU uint32 `json:"cpu"`
|
||||
// milicpus, 1000 milicpus = 1 cpu
|
||||
CPU uint32 `json:"cpu"`
|
||||
// MiB: 128MiB to 30GiB
|
||||
Memory uint32 `json:"memory"`
|
||||
}
|
||||
|
||||
@@ -537,17 +569,28 @@ type ConfigComputeResourcesUpdateInput struct {
|
||||
Memory *uint32 `json:"memory,omitempty"`
|
||||
}
|
||||
|
||||
// main entrypoint to the configuration
|
||||
type ConfigConfig struct {
|
||||
Ai *ConfigAi `json:"ai,omitempty"`
|
||||
Auth *ConfigAuth `json:"auth,omitempty"`
|
||||
Functions *ConfigFunctions `json:"functions,omitempty"`
|
||||
Global *ConfigGlobal `json:"global,omitempty"`
|
||||
Graphql *ConfigGraphql `json:"graphql,omitempty"`
|
||||
Hasura *ConfigHasura `json:"hasura"`
|
||||
// Configuration for graphite service
|
||||
Ai *ConfigAi `json:"ai,omitempty"`
|
||||
// Configuration for auth service
|
||||
Auth *ConfigAuth `json:"auth,omitempty"`
|
||||
// Configuration for functions service
|
||||
Functions *ConfigFunctions `json:"functions,omitempty"`
|
||||
// Global configuration that applies to all services
|
||||
Global *ConfigGlobal `json:"global,omitempty"`
|
||||
// Advanced configuration for GraphQL
|
||||
Graphql *ConfigGraphql `json:"graphql,omitempty"`
|
||||
// Configuration for hasura
|
||||
Hasura *ConfigHasura `json:"hasura"`
|
||||
// Configuration for observability service
|
||||
Observability *ConfigObservability `json:"observability"`
|
||||
Postgres *ConfigPostgres `json:"postgres"`
|
||||
Provider *ConfigProvider `json:"provider,omitempty"`
|
||||
Storage *ConfigStorage `json:"storage,omitempty"`
|
||||
// Configuration for postgres service
|
||||
Postgres *ConfigPostgres `json:"postgres"`
|
||||
// Configuration for third party providers like SMTP, SMS, etc.
|
||||
Provider *ConfigProvider `json:"provider,omitempty"`
|
||||
// Configuration for storage service
|
||||
Storage *ConfigStorage `json:"storage,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigConfigUpdateInput struct {
|
||||
@@ -564,7 +607,8 @@ type ConfigConfigUpdateInput struct {
|
||||
}
|
||||
|
||||
type ConfigEnvironmentVariable struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name"`
|
||||
// Value of the environment variable
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
@@ -578,6 +622,7 @@ type ConfigEnvironmentVariableUpdateInput struct {
|
||||
Value *string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// Configuration for functions service
|
||||
type ConfigFunctions struct {
|
||||
Node *ConfigFunctionsNode `json:"node,omitempty"`
|
||||
RateLimit *ConfigRateLimit `json:"rateLimit,omitempty"`
|
||||
@@ -606,12 +651,15 @@ type ConfigFunctionsUpdateInput struct {
|
||||
Resources *ConfigFunctionsResourcesUpdateInput `json:"resources,omitempty"`
|
||||
}
|
||||
|
||||
// Global configuration that applies to all services
|
||||
type ConfigGlobal struct {
|
||||
// User-defined environment variables that are spread over all services
|
||||
Environment []*ConfigGlobalEnvironmentVariable `json:"environment,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigGlobalEnvironmentVariable struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name"`
|
||||
// Value of the environment variable
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
@@ -768,23 +816,34 @@ type ConfigGraphqlUpdateInput struct {
|
||||
Security *ConfigGraphqlSecurityUpdateInput `json:"security,omitempty"`
|
||||
}
|
||||
|
||||
// Configuration for hasura service
|
||||
type ConfigHasura struct {
|
||||
AdminSecret string `json:"adminSecret"`
|
||||
AuthHook *ConfigHasuraAuthHook `json:"authHook,omitempty"`
|
||||
Events *ConfigHasuraEvents `json:"events,omitempty"`
|
||||
JwtSecrets []*ConfigJWTSecret `json:"jwtSecrets,omitempty"`
|
||||
Logs *ConfigHasuraLogs `json:"logs,omitempty"`
|
||||
RateLimit *ConfigRateLimit `json:"rateLimit,omitempty"`
|
||||
Resources *ConfigResources `json:"resources,omitempty"`
|
||||
Settings *ConfigHasuraSettings `json:"settings,omitempty"`
|
||||
Version *string `json:"version,omitempty"`
|
||||
WebhookSecret string `json:"webhookSecret"`
|
||||
// Admin secret
|
||||
AdminSecret string `json:"adminSecret"`
|
||||
AuthHook *ConfigHasuraAuthHook `json:"authHook,omitempty"`
|
||||
Events *ConfigHasuraEvents `json:"events,omitempty"`
|
||||
// JWT Secrets configuration
|
||||
JwtSecrets []*ConfigJWTSecret `json:"jwtSecrets,omitempty"`
|
||||
Logs *ConfigHasuraLogs `json:"logs,omitempty"`
|
||||
RateLimit *ConfigRateLimit `json:"rateLimit,omitempty"`
|
||||
// Resources for the service
|
||||
Resources *ConfigResources `json:"resources,omitempty"`
|
||||
// Configuration for hasura services
|
||||
// Reference: https://hasura.io/docs/latest/deployment/graphql-engine-flags/reference/
|
||||
Settings *ConfigHasuraSettings `json:"settings,omitempty"`
|
||||
// Version of hasura, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/hasura/graphql-engine/tags
|
||||
Version *string `json:"version,omitempty"`
|
||||
// Webhook secret
|
||||
WebhookSecret string `json:"webhookSecret"`
|
||||
}
|
||||
|
||||
type ConfigHasuraAuthHook struct {
|
||||
Mode *string `json:"mode,omitempty"`
|
||||
SendRequestBody *bool `json:"sendRequestBody,omitempty"`
|
||||
URL string `json:"url"`
|
||||
Mode *string `json:"mode,omitempty"`
|
||||
// HASURA_GRAPHQL_AUTH_HOOK_SEND_REQUEST_BODY
|
||||
SendRequestBody *bool `json:"sendRequestBody,omitempty"`
|
||||
// HASURA_GRAPHQL_AUTH_HOOK
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type ConfigHasuraAuthHookUpdateInput struct {
|
||||
@@ -794,6 +853,7 @@ type ConfigHasuraAuthHookUpdateInput struct {
|
||||
}
|
||||
|
||||
type ConfigHasuraEvents struct {
|
||||
// HASURA_GRAPHQL_EVENTS_HTTP_POOL_SIZE
|
||||
HTTPPoolSize *uint32 `json:"httpPoolSize,omitempty"`
|
||||
}
|
||||
|
||||
@@ -809,16 +869,27 @@ type ConfigHasuraLogsUpdateInput struct {
|
||||
Level *string `json:"level,omitempty"`
|
||||
}
|
||||
|
||||
// Configuration for hasura services
|
||||
// Reference: https://hasura.io/docs/latest/deployment/graphql-engine-flags/reference/
|
||||
type ConfigHasuraSettings struct {
|
||||
CorsDomain []string `json:"corsDomain,omitempty"`
|
||||
DevMode *bool `json:"devMode,omitempty"`
|
||||
EnableAllowList *bool `json:"enableAllowList,omitempty"`
|
||||
EnableConsole *bool `json:"enableConsole,omitempty"`
|
||||
EnableRemoteSchemaPermissions *bool `json:"enableRemoteSchemaPermissions,omitempty"`
|
||||
EnabledAPIs []string `json:"enabledAPIs,omitempty"`
|
||||
InferFunctionPermissions *bool `json:"inferFunctionPermissions,omitempty"`
|
||||
LiveQueriesMultiplexedRefetchInterval *uint32 `json:"liveQueriesMultiplexedRefetchInterval,omitempty"`
|
||||
StringifyNumericTypes *bool `json:"stringifyNumericTypes,omitempty"`
|
||||
// HASURA_GRAPHQL_CORS_DOMAIN
|
||||
CorsDomain []string `json:"corsDomain,omitempty"`
|
||||
// HASURA_GRAPHQL_DEV_MODE
|
||||
DevMode *bool `json:"devMode,omitempty"`
|
||||
// HASURA_GRAPHQL_ENABLE_ALLOWLIST
|
||||
EnableAllowList *bool `json:"enableAllowList,omitempty"`
|
||||
// HASURA_GRAPHQL_ENABLE_CONSOLE
|
||||
EnableConsole *bool `json:"enableConsole,omitempty"`
|
||||
// HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS
|
||||
EnableRemoteSchemaPermissions *bool `json:"enableRemoteSchemaPermissions,omitempty"`
|
||||
// HASURA_GRAPHQL_ENABLED_APIS
|
||||
EnabledAPIs []string `json:"enabledAPIs,omitempty"`
|
||||
// HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS
|
||||
InferFunctionPermissions *bool `json:"inferFunctionPermissions,omitempty"`
|
||||
// HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL
|
||||
LiveQueriesMultiplexedRefetchInterval *uint32 `json:"liveQueriesMultiplexedRefetchInterval,omitempty"`
|
||||
// HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES
|
||||
StringifyNumericTypes *bool `json:"stringifyNumericTypes,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigHasuraSettingsUpdateInput struct {
|
||||
@@ -891,6 +962,7 @@ type ConfigIngressUpdateInput struct {
|
||||
TLS *ConfigIngressTLSUpdateInput `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
// See https://hasura.io/docs/latest/auth/authentication/jwt/
|
||||
type ConfigJWTSecret struct {
|
||||
AllowedSkew *uint32 `json:"allowed_skew,omitempty"`
|
||||
Audience *string `json:"audience,omitempty"`
|
||||
@@ -939,11 +1011,15 @@ type ConfigObservabilityUpdateInput struct {
|
||||
Grafana *ConfigGrafanaUpdateInput `json:"grafana,omitempty"`
|
||||
}
|
||||
|
||||
// Configuration for postgres service
|
||||
type ConfigPostgres struct {
|
||||
Pitr *ConfigPostgresPitr `json:"pitr,omitempty"`
|
||||
Pitr *ConfigPostgresPitr `json:"pitr,omitempty"`
|
||||
// Resources for the service
|
||||
Resources *ConfigPostgresResources `json:"resources"`
|
||||
Settings *ConfigPostgresSettings `json:"settings,omitempty"`
|
||||
Version *string `json:"version,omitempty"`
|
||||
// Version of postgres, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/nhost/postgres/tags
|
||||
Version *string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigPostgresPitr struct {
|
||||
@@ -954,6 +1030,7 @@ type ConfigPostgresPitrUpdateInput struct {
|
||||
Retention *uint32 `json:"retention,omitempty"`
|
||||
}
|
||||
|
||||
// Resources for the service
|
||||
type ConfigPostgresResources struct {
|
||||
Compute *ConfigResourcesCompute `json:"compute,omitempty"`
|
||||
EnablePublicAccess *bool `json:"enablePublicAccess,omitempty"`
|
||||
@@ -1060,15 +1137,19 @@ type ConfigRateLimitUpdateInput struct {
|
||||
Limit *uint32 `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
// Resource configuration for a service
|
||||
type ConfigResources struct {
|
||||
Autoscaler *ConfigAutoscaler `json:"autoscaler,omitempty"`
|
||||
Compute *ConfigResourcesCompute `json:"compute,omitempty"`
|
||||
Networking *ConfigNetworking `json:"networking,omitempty"`
|
||||
Replicas *uint32 `json:"replicas,omitempty"`
|
||||
// Number of replicas for a service
|
||||
Replicas *uint32 `json:"replicas,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigResourcesCompute struct {
|
||||
CPU uint32 `json:"cpu"`
|
||||
// milicpus, 1000 milicpus = 1 cpu
|
||||
CPU uint32 `json:"cpu"`
|
||||
// MiB: 128MiB to 30GiB
|
||||
Memory uint32 `json:"memory"`
|
||||
}
|
||||
|
||||
@@ -1120,7 +1201,8 @@ type ConfigRunServiceConfigWithID struct {
|
||||
}
|
||||
|
||||
type ConfigRunServiceImage struct {
|
||||
Image string `json:"image"`
|
||||
Image string `json:"image"`
|
||||
// content of "auths", i.e., { "auths": $THIS }
|
||||
PullCredentials *string `json:"pullCredentials,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1158,11 +1240,13 @@ type ConfigRunServicePortUpdateInput struct {
|
||||
Type *string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Resource configuration for a service
|
||||
type ConfigRunServiceResources struct {
|
||||
Autoscaler *ConfigAutoscaler `json:"autoscaler,omitempty"`
|
||||
Compute *ConfigComputeResources `json:"compute"`
|
||||
Replicas uint32 `json:"replicas"`
|
||||
Storage []*ConfigRunServiceResourcesStorage `json:"storage,omitempty"`
|
||||
Autoscaler *ConfigAutoscaler `json:"autoscaler,omitempty"`
|
||||
Compute *ConfigComputeResources `json:"compute"`
|
||||
// Number of replicas for a service
|
||||
Replicas uint32 `json:"replicas"`
|
||||
Storage []*ConfigRunServiceResourcesStorage `json:"storage,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigRunServiceResourcesInsertInput struct {
|
||||
@@ -1173,9 +1257,11 @@ type ConfigRunServiceResourcesInsertInput struct {
|
||||
}
|
||||
|
||||
type ConfigRunServiceResourcesStorage struct {
|
||||
// GiB
|
||||
Capacity uint32 `json:"capacity"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
// name of the volume, changing it will cause data loss
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type ConfigRunServiceResourcesStorageInsertInput struct {
|
||||
@@ -1259,11 +1345,20 @@ type ConfigStandardOauthProviderWithScopeUpdateInput struct {
|
||||
Scope []string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// Configuration for storage service
|
||||
type ConfigStorage struct {
|
||||
Antivirus *ConfigStorageAntivirus `json:"antivirus,omitempty"`
|
||||
RateLimit *ConfigRateLimit `json:"rateLimit,omitempty"`
|
||||
Resources *ConfigResources `json:"resources,omitempty"`
|
||||
Version *string `json:"version,omitempty"`
|
||||
// Networking (custom domains at the moment) are not allowed as we need to do further
|
||||
// configurations in the CDN. We will enable it again in the future.
|
||||
Resources *ConfigResources `json:"resources,omitempty"`
|
||||
// Version of storage service, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/nhost/hasura-storage/tags
|
||||
//
|
||||
// Releases:
|
||||
//
|
||||
// https://github.com/nhost/hasura-storage/releases
|
||||
Version *string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigStorageAntivirus struct {
|
||||
@@ -1301,6 +1396,8 @@ type ConfigSystemConfigAuthEmailTemplates struct {
|
||||
}
|
||||
|
||||
type ConfigSystemConfigGraphql struct {
|
||||
// manually enable graphi on a per-service basis
|
||||
// by default it follows the plan
|
||||
FeatureAdvancedGraphql *bool `json:"featureAdvancedGraphql,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1718,7 +1815,8 @@ type Apps struct {
|
||||
AppStates []*AppStateHistory `json:"appStates"`
|
||||
AutomaticDeploys bool `json:"automaticDeploys"`
|
||||
// An array relationship
|
||||
Backups []*Backups `json:"backups"`
|
||||
Backups []*Backups `json:"backups"`
|
||||
// main entrypoint to the configuration
|
||||
Config *ConfigConfig `json:"config,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
// An object relationship
|
||||
@@ -2725,6 +2823,7 @@ type Deployments struct {
|
||||
CommitSha string `json:"commitSHA"`
|
||||
CommitUserAvatarURL *string `json:"commitUserAvatarUrl,omitempty"`
|
||||
CommitUserName *string `json:"commitUserName,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DeploymentEndedAt *time.Time `json:"deploymentEndedAt,omitempty"`
|
||||
// An array relationship
|
||||
DeploymentLogs []*DeploymentLogs `json:"deploymentLogs"`
|
||||
@@ -2767,6 +2866,7 @@ type DeploymentsBoolExp struct {
|
||||
CommitSha *StringComparisonExp `json:"commitSHA,omitempty"`
|
||||
CommitUserAvatarURL *StringComparisonExp `json:"commitUserAvatarUrl,omitempty"`
|
||||
CommitUserName *StringComparisonExp `json:"commitUserName,omitempty"`
|
||||
CreatedAt *TimestamptzComparisonExp `json:"createdAt,omitempty"`
|
||||
DeploymentEndedAt *TimestamptzComparisonExp `json:"deploymentEndedAt,omitempty"`
|
||||
DeploymentLogs *DeploymentLogsBoolExp `json:"deploymentLogs,omitempty"`
|
||||
DeploymentStartedAt *TimestamptzComparisonExp `json:"deploymentStartedAt,omitempty"`
|
||||
@@ -2801,6 +2901,7 @@ type DeploymentsMaxOrderBy struct {
|
||||
CommitSha *OrderBy `json:"commitSHA,omitempty"`
|
||||
CommitUserAvatarURL *OrderBy `json:"commitUserAvatarUrl,omitempty"`
|
||||
CommitUserName *OrderBy `json:"commitUserName,omitempty"`
|
||||
CreatedAt *OrderBy `json:"createdAt,omitempty"`
|
||||
DeploymentEndedAt *OrderBy `json:"deploymentEndedAt,omitempty"`
|
||||
DeploymentStartedAt *OrderBy `json:"deploymentStartedAt,omitempty"`
|
||||
DeploymentStatus *OrderBy `json:"deploymentStatus,omitempty"`
|
||||
@@ -2823,6 +2924,7 @@ type DeploymentsMinOrderBy struct {
|
||||
CommitSha *OrderBy `json:"commitSHA,omitempty"`
|
||||
CommitUserAvatarURL *OrderBy `json:"commitUserAvatarUrl,omitempty"`
|
||||
CommitUserName *OrderBy `json:"commitUserName,omitempty"`
|
||||
CreatedAt *OrderBy `json:"createdAt,omitempty"`
|
||||
DeploymentEndedAt *OrderBy `json:"deploymentEndedAt,omitempty"`
|
||||
DeploymentStartedAt *OrderBy `json:"deploymentStartedAt,omitempty"`
|
||||
DeploymentStatus *OrderBy `json:"deploymentStatus,omitempty"`
|
||||
@@ -2861,6 +2963,7 @@ type DeploymentsOrderBy struct {
|
||||
CommitSha *OrderBy `json:"commitSHA,omitempty"`
|
||||
CommitUserAvatarURL *OrderBy `json:"commitUserAvatarUrl,omitempty"`
|
||||
CommitUserName *OrderBy `json:"commitUserName,omitempty"`
|
||||
CreatedAt *OrderBy `json:"createdAt,omitempty"`
|
||||
DeploymentEndedAt *OrderBy `json:"deploymentEndedAt,omitempty"`
|
||||
DeploymentLogsAggregate *DeploymentLogsAggregateOrderBy `json:"deploymentLogs_aggregate,omitempty"`
|
||||
DeploymentStartedAt *OrderBy `json:"deploymentStartedAt,omitempty"`
|
||||
@@ -2892,6 +2995,7 @@ type DeploymentsStreamCursorValueInput struct {
|
||||
CommitSha *string `json:"commitSHA,omitempty"`
|
||||
CommitUserAvatarURL *string `json:"commitUserAvatarUrl,omitempty"`
|
||||
CommitUserName *string `json:"commitUserName,omitempty"`
|
||||
CreatedAt *time.Time `json:"createdAt,omitempty"`
|
||||
DeploymentEndedAt *time.Time `json:"deploymentEndedAt,omitempty"`
|
||||
DeploymentStartedAt *time.Time `json:"deploymentStartedAt,omitempty"`
|
||||
DeploymentStatus *string `json:"deploymentStatus,omitempty"`
|
||||
@@ -6885,6 +6989,8 @@ const (
|
||||
// column name
|
||||
DeploymentsSelectColumnCommitUserName DeploymentsSelectColumn = "commitUserName"
|
||||
// column name
|
||||
DeploymentsSelectColumnCreatedAt DeploymentsSelectColumn = "createdAt"
|
||||
// column name
|
||||
DeploymentsSelectColumnDeploymentEndedAt DeploymentsSelectColumn = "deploymentEndedAt"
|
||||
// column name
|
||||
DeploymentsSelectColumnDeploymentStartedAt DeploymentsSelectColumn = "deploymentStartedAt"
|
||||
@@ -6918,6 +7024,7 @@ var AllDeploymentsSelectColumn = []DeploymentsSelectColumn{
|
||||
DeploymentsSelectColumnCommitSha,
|
||||
DeploymentsSelectColumnCommitUserAvatarURL,
|
||||
DeploymentsSelectColumnCommitUserName,
|
||||
DeploymentsSelectColumnCreatedAt,
|
||||
DeploymentsSelectColumnDeploymentEndedAt,
|
||||
DeploymentsSelectColumnDeploymentStartedAt,
|
||||
DeploymentsSelectColumnDeploymentStatus,
|
||||
@@ -6935,7 +7042,7 @@ var AllDeploymentsSelectColumn = []DeploymentsSelectColumn{
|
||||
|
||||
func (e DeploymentsSelectColumn) IsValid() bool {
|
||||
switch e {
|
||||
case DeploymentsSelectColumnAppID, DeploymentsSelectColumnCommitMessage, DeploymentsSelectColumnCommitSha, DeploymentsSelectColumnCommitUserAvatarURL, DeploymentsSelectColumnCommitUserName, DeploymentsSelectColumnDeploymentEndedAt, DeploymentsSelectColumnDeploymentStartedAt, DeploymentsSelectColumnDeploymentStatus, DeploymentsSelectColumnFunctionsEndedAt, DeploymentsSelectColumnFunctionsStartedAt, DeploymentsSelectColumnFunctionsStatus, DeploymentsSelectColumnID, DeploymentsSelectColumnMetadataEndedAt, DeploymentsSelectColumnMetadataStartedAt, DeploymentsSelectColumnMetadataStatus, DeploymentsSelectColumnMigrationsEndedAt, DeploymentsSelectColumnMigrationsStartedAt, DeploymentsSelectColumnMigrationsStatus:
|
||||
case DeploymentsSelectColumnAppID, DeploymentsSelectColumnCommitMessage, DeploymentsSelectColumnCommitSha, DeploymentsSelectColumnCommitUserAvatarURL, DeploymentsSelectColumnCommitUserName, DeploymentsSelectColumnCreatedAt, DeploymentsSelectColumnDeploymentEndedAt, DeploymentsSelectColumnDeploymentStartedAt, DeploymentsSelectColumnDeploymentStatus, DeploymentsSelectColumnFunctionsEndedAt, DeploymentsSelectColumnFunctionsStartedAt, DeploymentsSelectColumnFunctionsStatus, DeploymentsSelectColumnID, DeploymentsSelectColumnMetadataEndedAt, DeploymentsSelectColumnMetadataStartedAt, DeploymentsSelectColumnMetadataStatus, DeploymentsSelectColumnMigrationsEndedAt, DeploymentsSelectColumnMigrationsStartedAt, DeploymentsSelectColumnMigrationsStatus:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -3,12 +3,13 @@ NEXT_PUBLIC_ENV=dev
|
||||
NEXT_PUBLIC_NHOST_PLATFORM=false
|
||||
|
||||
# Environment Variables for Self Hosting and Local Development
|
||||
NEXT_PUBLIC_NHOST_AUTH_URL=https://local.auth.nhost.local.run/v1
|
||||
NEXT_PUBLIC_NHOST_AUTH_URL=https://local.auth.local.nhost.run/v1
|
||||
NEXT_PUBLIC_NHOST_CONFIGSERVER_URL=https://local.dashboard.local.nhost.run/v1/configserver/graphql
|
||||
NEXT_PUBLIC_NHOST_FUNCTIONS_URL=https://local.functions.local.nhost.run/v1
|
||||
NEXT_PUBLIC_NHOST_GRAPHQL_URL=https://local.graphql.local.nhost.run/v1
|
||||
NEXT_PUBLIC_NHOST_STORAGE_URL=https://local.storage.local.nhost.run/v1
|
||||
NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL=https://local.hasura.local.nhost.run
|
||||
NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL=https://local.hasura.local.nhost.run/v1/migrations
|
||||
NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL=https://local.hasura.local.nhost.run/console
|
||||
NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL=https://local.hasura.local.nhost.run/apis/migrate
|
||||
NEXT_PUBLIC_NHOST_HASURA_API_URL=https://local.hasura.local.nhost.run
|
||||
|
||||
# Environment Variables when running the Nhost Dashboard against the Nhost Backend
|
||||
@@ -18,13 +19,13 @@ NEXT_PUBLIC_ANALYTICS_WRITE_KEY=<analytics_write_key>
|
||||
NEXT_PUBLIC_SEGMENT_CDN_URL=<segment_cdn_url>
|
||||
NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET=<nhost_bragi_websocket>
|
||||
|
||||
NEXT_PUBLIC_ZENDESK_URL=
|
||||
NEXT_PUBLIC_ZENDESK_API_KEY=
|
||||
NEXT_PUBLIC_ZENDESK_USER_EMAIL=
|
||||
NEXT_ZENDESK_URL=
|
||||
NEXT_ZENDESK_API_KEY=
|
||||
NEXT_ZENDESK_USER_EMAIL=
|
||||
|
||||
|
||||
CODEGEN_GRAPHQL_URL=https://local.graphql.local.nhost.run/v1
|
||||
CODEGEN_HASURA_ADMIN_SECRET=nhost-admin-secret
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY=FIXME
|
||||
|
||||
NEXT_PUBLIC_SOC2_REPORT_FILE_ID=
|
||||
NEXT_PUBLIC_SOC2_REPORT_FILE_ID=
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'storybook-addon-next-router',
|
||||
{
|
||||
/**
|
||||
* Fix Storybook issue with PostCSS@8
|
||||
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
|
||||
*/
|
||||
name: '@storybook/addon-postcss',
|
||||
options: {
|
||||
postcssLoaderOptions: {
|
||||
implementation: require('postcss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
framework: '@storybook/react',
|
||||
core: {
|
||||
builder: '@storybook/builder-webpack5',
|
||||
},
|
||||
features: {
|
||||
emotionAlias: true,
|
||||
},
|
||||
webpackFinal: async (config) => {
|
||||
return {
|
||||
...config,
|
||||
resolve: {
|
||||
...config?.resolve,
|
||||
plugins: [
|
||||
...(config?.resolve?.plugins || []),
|
||||
new TsconfigPathsPlugin(),
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
env: (config) => ({
|
||||
...config,
|
||||
NEXT_PUBLIC_ENV: 'dev',
|
||||
NEXT_PUBLIC_NHOST_PLATFORM: 'false',
|
||||
}),
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import { NhostProvider } from '@/providers/nhost';
|
||||
import '@fontsource/inter';
|
||||
import '@fontsource/inter/500.css';
|
||||
import '@fontsource/inter/700.css';
|
||||
import { CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import { createClient } from '@nhost/nhost-js-beta';
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Buffer } from 'buffer';
|
||||
import { initialize, mswDecorator } from 'msw-storybook-addon';
|
||||
import { RouterContext } from 'next/dist/shared/lib/router-context';
|
||||
import { createTheme } from '../src/components/ui/v2/createTheme';
|
||||
import '../src/styles/globals.css';
|
||||
|
||||
global.Buffer = Buffer;
|
||||
|
||||
initialize({ onUnhandledRequest: 'bypass' });
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export const parameters = {
|
||||
nextRouter: {
|
||||
Provider: RouterContext.Provider,
|
||||
isReady: true,
|
||||
},
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
(Story, context) => {
|
||||
const isDarkMode = !context.globals?.backgrounds?.value
|
||||
?.toLowerCase()
|
||||
?.startsWith('#f');
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={createTheme(isDarkMode ? 'dark' : 'light')}>
|
||||
<CssBaseline />
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
);
|
||||
},
|
||||
(Story) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
),
|
||||
(Story) => (
|
||||
<NhostApolloProvider
|
||||
fetchPolicy="cache-first"
|
||||
graphqlUrl="https://local.graphql.nhost.run/v1"
|
||||
>
|
||||
<Story />
|
||||
</NhostApolloProvider>
|
||||
),
|
||||
(Story) => (
|
||||
<NhostProvider
|
||||
nhost={createClient({ subdomain: 'local', region: 'local' })}
|
||||
>
|
||||
<Story />
|
||||
</NhostProvider>
|
||||
),
|
||||
mswDecorator,
|
||||
];
|
||||
@@ -2,6 +2,34 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [@nhost/dashboard@2.40.0] - 2025-10-27
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(dashboard)* Allow configuring CSP header (#3627)
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(dashboard)* Various improvements to support ticket page (#3630)
|
||||
|
||||
## [@nhost/dashboard@2.39.0] - 2025-10-22
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(dashboard)* Move zendesk request to API route (#3628)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(dashboard)* Fix flaky e2e tests (#3536)
|
||||
- *(dashboard)* Run audit and lint in dashboard (#3578)
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(dashboard)* Cleanup e2e remote schemas test before run (#3581)
|
||||
|
||||
## [@nhost/dashboard@2.38.4] - 2025-10-09
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
@@ -62,20 +62,6 @@ NEXT_PUBLIC_NHOST_HASURA_API_URL=https://local.hasura.local.nhost.run
|
||||
|
||||
This will connect the Nhost Dashboard to your locally running Nhost backend.
|
||||
|
||||
### Storybook
|
||||
|
||||
Components are documented using [Storybook](https://storybook.js.org/). To run Storybook, run the following command:
|
||||
|
||||
```bash
|
||||
pnpm storybook
|
||||
```
|
||||
|
||||
By default, Storybook will run on port `6006`. You can change this by passing the `--port` flag:
|
||||
|
||||
```bash
|
||||
pnpm storybook --port 6007
|
||||
```
|
||||
|
||||
### General Environment Variables
|
||||
|
||||
| Name | Description |
|
||||
@@ -96,6 +82,15 @@ pnpm storybook --port 6007
|
||||
| `NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL` | The URL of Hasura's Migrations service. When working locally, point it to the Migrations service started by the CLI. |
|
||||
| `NEXT_PUBLIC_NHOST_HASURA_API_URL` | The URL of Hasura's Schema and Metadata API. When working locally, point it to the Schema and Metadata API started by the CLI. When self-hosting, point it to the self-hosted Schema and Metadata API. |
|
||||
|
||||
### Content Security Policy (CSP) Configuration
|
||||
|
||||
The dashboard supports build-time CSP configuration to enable self-hosted deployments on custom domains.
|
||||
|
||||
| Name | Description |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `CSP_MODE` | Controls CSP behavior. Options: `nhost` (default, uses Nhost Cloud CSP), `disabled` (no CSP headers), `custom` (use custom CSP via `CSP_HEADER`). For self-hosted deployments on custom domains, set to `disabled` or `custom`. |
|
||||
| `CSP_HEADER` | Custom Content Security Policy header value. Only used when `CSP_MODE=custom`. Should be a complete CSP string (e.g., `default-src 'self'; script-src 'self' 'unsafe-eval'; ...`). |
|
||||
|
||||
### Other Environment Variables
|
||||
|
||||
| Name | Description |
|
||||
|
||||
@@ -105,8 +105,8 @@ test('should create a table with nullable columns', async ({
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
||||
.click();
|
||||
await page.getByText('Edit Table').click();
|
||||
expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
||||
expect(page.locator('div[data-testid="id"]')).toBeVisible();
|
||||
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
||||
await expect(page.locator('div[data-testid="id"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create a table with an identity column', async ({
|
||||
@@ -146,13 +146,13 @@ test('should create a table with an identity column', async ({
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
||||
.click();
|
||||
await page.getByText('Edit Table').click();
|
||||
expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
||||
expect(
|
||||
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('button#identityColumnIndex :has-text("identity_column")'),
|
||||
).toBeVisible();
|
||||
expect(page.locator('[id="columns.3.defaultValue"]')).toBeDisabled();
|
||||
expect(page.locator('[name="columns.3.isNullable"]')).toBeDisabled();
|
||||
expect(page.locator('[name="columns.3.isUnique"]')).toBeDisabled();
|
||||
await expect(page.locator('[id="columns.3.defaultValue"]')).toBeDisabled();
|
||||
await expect(page.locator('[name="columns.3.isNullable"]')).toBeDisabled();
|
||||
await expect(page.locator('[name="columns.3.isUnique"]')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should create table with foreign key constraint', async ({
|
||||
@@ -234,6 +234,46 @@ test('should create table with foreign key constraint', async ({
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be able to create a table with a composite key', async ({
|
||||
authenticatedNhostPage: page,
|
||||
}) => {
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
const tableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKeys: ['id', 'second_id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'second_id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'name', type: 'text' },
|
||||
],
|
||||
});
|
||||
|
||||
await expect(page.locator('div[data-testid="id"]')).toBeVisible();
|
||||
await expect(page.locator('div[data-testid="second_id"]')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: tableName, exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
||||
.click();
|
||||
await page.getByText('Edit Table').click();
|
||||
await expect(page.locator('div[data-testid="id"]')).toBeVisible();
|
||||
await expect(page.locator('div[data-testid="second_id"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not be able to create a table with a name that already exists', async ({
|
||||
authenticatedNhostPage: page,
|
||||
}) => {
|
||||
@@ -280,40 +320,3 @@ test('should not be able to create a table with a name that already exists', asy
|
||||
page.getByText(/error: a table with this name already exists/i),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be able to create a table with a composite key', async ({
|
||||
authenticatedNhostPage: page,
|
||||
}) => {
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
const tableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKeys: ['id', 'second_id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'second_id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'name', type: 'text' },
|
||||
],
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: tableName, exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
||||
.click();
|
||||
await page.getByText('Edit Table').click();
|
||||
expect(page.locator('div[data-testid="id"]')).toBeVisible();
|
||||
expect(page.locator('div[data-testid="second_id"]')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -41,12 +41,13 @@ export const TEST_USER_PASSWORD = process.env.NHOST_TEST_USER_PASSWORD!;
|
||||
|
||||
export const TEST_PERSONAL_ORG_SLUG = process.env.NHOST_TEST_PERSONAL_ORG_SLUG!;
|
||||
|
||||
const freeUserEmails = process.env.NHOST_TEST_FREE_USER_EMAILS!;
|
||||
|
||||
export const TEST_FREE_USER_EMAILS: string[] = JSON.parse(freeUserEmails);
|
||||
export const TEST_ONBOARDING_USER = process.env.NHOST_TEST_ONBOARDING_USER!;
|
||||
|
||||
/**
|
||||
* Name of the remote schema serverless function to test against.
|
||||
*/
|
||||
export const TEST_PROJECT_REMOTE_SCHEMA_NAME =
|
||||
process.env.NHOST_TEST_PROJECT_REMOTE_SCHEMA_NAME!;
|
||||
|
||||
export const TEST_STAGING_SUBDOMAIN = process.env.NHOST_TEST_STAGING_SUBDOMAIN!;
|
||||
export const TEST_STAGING_REGION = process.env.NHOST_TEST_STAGING_REGION!;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { expect, test } from '@/e2e/fixtures/auth-hook';
|
||||
import {
|
||||
cleanupOnboardingTestIfNeeded,
|
||||
getCardExpiration,
|
||||
getOrgSlugFromUrl,
|
||||
getProjectSlugFromUrl,
|
||||
gotoUrl,
|
||||
loginWithFreeUser,
|
||||
setFreeUserStarterOrgSlug,
|
||||
setNewProjectName,
|
||||
setNewProjectSlug,
|
||||
} from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
@@ -15,13 +12,15 @@ import type { Page } from '@playwright/test';
|
||||
let page: Page;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
await cleanupOnboardingTestIfNeeded();
|
||||
|
||||
page = await browser.newPage();
|
||||
await loginWithFreeUser(page);
|
||||
});
|
||||
|
||||
test('user should be able to finish onboarding', async () => {
|
||||
await gotoUrl(page, `/onboarding`);
|
||||
expect(page.getByText('Welcome to Nhost!')).toBeVisible();
|
||||
await expect(page.getByText('Welcome to Nhost!')).toBeVisible();
|
||||
const organizationName = faker.lorem.words(3).slice(0, 32);
|
||||
|
||||
await page.getByLabel('Organization Name').fill(organizationName);
|
||||
@@ -68,34 +67,28 @@ test('user should be able to finish onboarding', async () => {
|
||||
.getByTestId('hosted-payment-submit-button')
|
||||
.click({ force: true });
|
||||
|
||||
expect(
|
||||
await expect(
|
||||
page.getByText('Processing new organization request').first(),
|
||||
).toBeVisible();
|
||||
await page.waitForSelector(
|
||||
'div:has-text("Organization created successfully. Redirecting...")',
|
||||
);
|
||||
|
||||
expect(page.getByText('Create Your First Project')).toBeVisible();
|
||||
await expect(page.getByText('Create Your First Project')).toBeVisible();
|
||||
|
||||
const projectName = faker.lorem.words(3).slice(0, 32);
|
||||
await page.getByLabel('Project Name').fill(projectName);
|
||||
|
||||
await page.getByText('Create Project', { exact: true }).click();
|
||||
|
||||
expect(page.getByText('Creating your project...')).toBeVisible();
|
||||
expect(page.getByText('Project created successfully!')).toBeVisible();
|
||||
await expect(page.getByText('Creating your project...')).toBeVisible();
|
||||
await expect(page.getByText('Project created successfully!')).toBeVisible();
|
||||
|
||||
expect(page.getByText('Internal info')).toBeVisible();
|
||||
await expect(page.getByText('Internal info')).toBeVisible();
|
||||
|
||||
await page.waitForSelector('h3:has-text("Project Health")', {
|
||||
timeout: 180000,
|
||||
});
|
||||
|
||||
const newProjectSlug = getProjectSlugFromUrl(page.url());
|
||||
setNewProjectSlug(newProjectSlug);
|
||||
setNewProjectName(organizationName);
|
||||
const newOrgSlug = getOrgSlugFromUrl(page.url());
|
||||
setFreeUserStarterOrgSlug(newOrgSlug);
|
||||
});
|
||||
|
||||
test('should delete the new organization', async () => {
|
||||
@@ -107,12 +100,12 @@ test('should delete the new organization', async () => {
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await page.waitForSelector('h2:has-text("Delete Organization")');
|
||||
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
await expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
|
||||
await page.getByLabel("I'm sure I want to delete this Organization").click();
|
||||
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
await expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
await page.getByLabel('I understand this action cannot be undone').click();
|
||||
expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
|
||||
await expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
|
||||
|
||||
await page.getByTestId('deleteOrgButton').click();
|
||||
|
||||
@@ -145,7 +138,7 @@ test('should be able to upgrade an organization', async () => {
|
||||
await page.getByRole('link', { name: 'Billing' }).click();
|
||||
|
||||
await page.waitForSelector('h4:has-text("Subscription plan")');
|
||||
expect(page.getByText('Upgrade')).toBeEnabled();
|
||||
await expect(page.getByText('Upgrade')).toBeEnabled();
|
||||
await page.getByText('Upgrade').click();
|
||||
await page.waitForSelector('h2:has-text("Upgrade Organization")');
|
||||
|
||||
@@ -205,12 +198,12 @@ test('should be able to upgrade an organization', async () => {
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await page.waitForSelector('h2:has-text("Delete Organization")');
|
||||
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
await expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
|
||||
await page.getByLabel("I'm sure I want to delete this Organization").click();
|
||||
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
await expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
await page.getByLabel('I understand this action cannot be undone').click();
|
||||
expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
|
||||
await expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
|
||||
|
||||
await page.getByTestId('deleteOrgButton').click();
|
||||
|
||||
|
||||
@@ -4,61 +4,64 @@ import {
|
||||
TEST_PROJECT_SUBDOMAIN,
|
||||
} from '@/e2e/env';
|
||||
import { expect, test } from '@/e2e/fixtures/auth-hook';
|
||||
import { cleanupRemoteSchemaTestIfNeeded } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { snakeCase } from 'snake-case';
|
||||
|
||||
const REMOTE_SCHEMA_TEST_URL = `https://${TEST_PROJECT_SUBDOMAIN}.functions.eu-central-1.staging.nhost.run/v1/${TEST_PROJECT_REMOTE_SCHEMA_NAME}`;
|
||||
|
||||
test.describe('Remote Schemas', () => {
|
||||
test.beforeEach(async ({ authenticatedNhostPage: page }) => {
|
||||
const remoteSchemasRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/graphql/remote-schemas`;
|
||||
await page.goto(remoteSchemasRoute);
|
||||
await page.waitForURL(remoteSchemasRoute);
|
||||
});
|
||||
|
||||
test('should create and delete a remote schema from URL', async ({
|
||||
authenticatedNhostPage: page,
|
||||
}) => {
|
||||
await page.getByRole('button', { name: /add remote schema/i }).click();
|
||||
await expect(page.getByText(/create a new remote schema/i)).toBeVisible();
|
||||
|
||||
const schemaName = snakeCase(`e2e ${faker.lorem.words(2)}`);
|
||||
|
||||
await page.getByPlaceholder(/remote schema name/i).fill(schemaName);
|
||||
await page
|
||||
.getByPlaceholder(/graphql-service\.example\.com/i)
|
||||
.fill(REMOTE_SCHEMA_TEST_URL);
|
||||
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForSelector(
|
||||
'div:has-text("The remote schema has been created successfully.")',
|
||||
);
|
||||
|
||||
const detailsUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/graphql/remote-schemas/${schemaName}`;
|
||||
await page.waitForURL(detailsUrl);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: schemaName, exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
const schemaLink = page.getByRole('link', {
|
||||
name: schemaName,
|
||||
exact: true,
|
||||
});
|
||||
|
||||
await schemaLink.hover();
|
||||
await page
|
||||
.getByRole('listitem')
|
||||
.filter({ hasText: schemaName })
|
||||
.getByRole('button')
|
||||
.click();
|
||||
|
||||
await page.getByRole('menuitem', { name: /delete remote schema/i }).click();
|
||||
await page.getByRole('button', { name: /^delete$/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: schemaName, exact: true }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
test.beforeAll(async () => {
|
||||
await cleanupRemoteSchemaTestIfNeeded();
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ authenticatedNhostPage: page }) => {
|
||||
const remoteSchemasRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/graphql/remote-schemas`;
|
||||
await page.goto(remoteSchemasRoute);
|
||||
await page.waitForURL(remoteSchemasRoute);
|
||||
});
|
||||
|
||||
test('should create and delete a remote schema from URL', async ({
|
||||
authenticatedNhostPage: page,
|
||||
}) => {
|
||||
await page.getByRole('button', { name: /add remote schema/i }).click();
|
||||
await expect(page.getByText(/create a new remote schema/i)).toBeVisible();
|
||||
|
||||
const schemaName = snakeCase(`e2e ${faker.lorem.words(2)}`);
|
||||
|
||||
await page.getByPlaceholder(/remote schema name/i).fill(schemaName);
|
||||
await page
|
||||
.getByPlaceholder(/graphql-service\.example\.com/i)
|
||||
.fill(REMOTE_SCHEMA_TEST_URL);
|
||||
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForSelector(
|
||||
'div:has-text("The remote schema has been created successfully.")',
|
||||
);
|
||||
|
||||
const detailsUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/graphql/remote-schemas/${schemaName}`;
|
||||
await page.waitForURL(detailsUrl);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: schemaName, exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
const schemaLink = page.getByRole('link', {
|
||||
name: schemaName,
|
||||
exact: true,
|
||||
});
|
||||
|
||||
await schemaLink.hover();
|
||||
await page
|
||||
.getByRole('listitem')
|
||||
.filter({ hasText: schemaName })
|
||||
.getByRole('button')
|
||||
.click();
|
||||
|
||||
await page.getByRole('menuitem', { name: /delete remote schema/i }).click();
|
||||
await page.getByRole('button', { name: /^delete$/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: schemaName, exact: true }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import {
|
||||
TEST_FREE_USER_EMAILS,
|
||||
TEST_ONBOARDING_USER,
|
||||
TEST_ORGANIZATION_SLUG,
|
||||
TEST_PROJECT_ADMIN_SECRET,
|
||||
TEST_PROJECT_SUBDOMAIN,
|
||||
TEST_STAGING_REGION,
|
||||
TEST_STAGING_SUBDOMAIN,
|
||||
TEST_USER_PASSWORD,
|
||||
} from '@/e2e/env';
|
||||
import { expect } from '@/e2e/fixtures/auth-hook';
|
||||
import { isEmptyValue } from '@/lib/utils';
|
||||
import type { ExportMetadataResponse } from '@/utils/hasura-api/generated/schemas';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { type Page } from '@playwright/test';
|
||||
import { add, format } from 'date-fns-v4';
|
||||
@@ -125,12 +132,26 @@ export async function prepareTable({
|
||||
),
|
||||
);
|
||||
await page.getByLabel('Primary Key').click();
|
||||
await Promise.all(
|
||||
primaryKeys.map(async (primaryKey) => {
|
||||
await page.getByRole('option', { name: primaryKey, exact: true }).click();
|
||||
}),
|
||||
);
|
||||
|
||||
await page
|
||||
.getByRole('option', { name: columns[0].name, exact: true })
|
||||
.waitFor({ timeout: 1000 });
|
||||
await expect(
|
||||
page.getByRole('option', { name: columns[0].name, exact: true }),
|
||||
).toBeVisible();
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const primaryKey of primaryKeys) {
|
||||
await page.waitForTimeout(1000);
|
||||
await page.getByRole('option', { name: primaryKey, exact: true }).click();
|
||||
await page
|
||||
.locator(`div[data-testid="${primaryKey}"]`)
|
||||
.waitFor({ timeout: 1000 });
|
||||
}
|
||||
await page.getByText('Create a New Table').click();
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(
|
||||
page.getByRole('option', { name: columns[0].name, exact: true }),
|
||||
).not.toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,42 +253,6 @@ export async function gotoUrl(page: Page, url: string) {
|
||||
await page.waitForURL(url, { waitUntil: 'load' });
|
||||
}
|
||||
|
||||
let newOrgSlug: string;
|
||||
|
||||
export function getNewOrgSlug() {
|
||||
return newOrgSlug;
|
||||
}
|
||||
|
||||
export function setNewOrgSlug(slug: string) {
|
||||
newOrgSlug = slug;
|
||||
}
|
||||
|
||||
let freeUserStarterOrgSlug: string;
|
||||
|
||||
export function getFreeUserStarterOrgSlug() {
|
||||
return freeUserStarterOrgSlug;
|
||||
}
|
||||
|
||||
export function setFreeUserStarterOrgSlug(slug: string) {
|
||||
freeUserStarterOrgSlug = slug;
|
||||
}
|
||||
|
||||
let newProjectSlug: string;
|
||||
|
||||
export function getNewProjectSlug() {
|
||||
return newProjectSlug;
|
||||
}
|
||||
|
||||
export function setNewProjectSlug(slug: string) {
|
||||
newProjectSlug = slug;
|
||||
}
|
||||
|
||||
export function getProjectSlugFromUrl(url: string) {
|
||||
const [, projectSlug] = url.split('/projects/');
|
||||
|
||||
return projectSlug;
|
||||
}
|
||||
|
||||
export function getOrgSlugFromUrl(url: string) {
|
||||
const orgSlug = url.split('/orgs/')[1].split('/projects/')[0];
|
||||
return orgSlug;
|
||||
@@ -278,33 +263,13 @@ export function getCardExpiration() {
|
||||
return format(now, 'MMyy');
|
||||
}
|
||||
|
||||
let newProjectName: string;
|
||||
|
||||
export function getNewProjectName() {
|
||||
return newProjectName;
|
||||
}
|
||||
|
||||
export function setNewProjectName(name: string) {
|
||||
newProjectName = name;
|
||||
}
|
||||
|
||||
function getRandomUserIndex(): number {
|
||||
return Math.floor(Math.random() * TEST_FREE_USER_EMAILS.length);
|
||||
}
|
||||
|
||||
export async function loginWithFreeUser(page: Page) {
|
||||
const userIndex = getRandomUserIndex();
|
||||
|
||||
const freeUserEmail = TEST_FREE_USER_EMAILS[userIndex];
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Selected userIndex: ${userIndex}`);
|
||||
await page.goto('/');
|
||||
await page.waitForURL('/signin');
|
||||
await page.getByRole('link', { name: /continue with email/i }).click();
|
||||
|
||||
await page.waitForURL('/signin/email');
|
||||
await page.getByLabel('Email').fill(freeUserEmail);
|
||||
await page.getByLabel('Email').fill(TEST_ONBOARDING_USER);
|
||||
await page.getByLabel('Password').fill(TEST_USER_PASSWORD);
|
||||
await page.getByRole('button', { name: /sign in/i }).click();
|
||||
|
||||
@@ -319,3 +284,132 @@ export function toPascalCase(str: string, divider = ' ') {
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join('');
|
||||
}
|
||||
|
||||
export async function cleanupOnboardingTestIfNeeded() {
|
||||
const signinUrl = `https://${TEST_STAGING_SUBDOMAIN}.auth.${TEST_STAGING_REGION}.nhost.run/v1/signin/email-password`;
|
||||
const graphqlUrl = `https://${TEST_STAGING_SUBDOMAIN}.graphql.${TEST_STAGING_REGION}.nhost.run/v1`;
|
||||
|
||||
try {
|
||||
const response = await fetch(signinUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: TEST_ONBOARDING_USER,
|
||||
password: TEST_USER_PASSWORD,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
const userId = data.session?.user?.id;
|
||||
const accessToken = data.session?.accessToken;
|
||||
const organizationPayload = {
|
||||
query: `
|
||||
query {
|
||||
organizations(where: { members: {userID: {_eq: "${userId}"}} }) {
|
||||
id
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
const authHeader = `Bearer ${accessToken}`;
|
||||
|
||||
const orgResponse = await fetch(graphqlUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: authHeader,
|
||||
},
|
||||
body: JSON.stringify(organizationPayload),
|
||||
});
|
||||
|
||||
const orgData = await orgResponse.json();
|
||||
|
||||
const organizations = orgData.data?.organizations;
|
||||
|
||||
if (organizations && organizations.length > 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Cleaning up organization');
|
||||
await Promise.all(
|
||||
organizations.map(({ id }) =>
|
||||
fetch(graphqlUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: authHeader,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
mutation {
|
||||
billingDeleteOrganization(organizationID: "${id}")
|
||||
}
|
||||
`,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupRemoteSchemaTestIfNeeded() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://${TEST_PROJECT_SUBDOMAIN}.hasura.eu-central-1.staging.nhost.run/v1/metadata`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-hasura-admin-secret': TEST_PROJECT_ADMIN_SECRET,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'export_metadata',
|
||||
version: 2,
|
||||
args: {},
|
||||
}),
|
||||
},
|
||||
);
|
||||
const data = (await response.json()) as ExportMetadataResponse;
|
||||
|
||||
const remoteSchemas = data.metadata?.remote_schemas;
|
||||
|
||||
if (isEmptyValue(remoteSchemas)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const schemasToDelete = remoteSchemas!.filter((remoteSchema) =>
|
||||
/^e2e_\w+_\w+$/.test(remoteSchema.name),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
schemasToDelete.map((remoteSchema) =>
|
||||
fetch(
|
||||
`https://${TEST_PROJECT_SUBDOMAIN}.hasura.eu-central-1.staging.nhost.run/v1/metadata`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-hasura-admin-secret': TEST_PROJECT_ADMIN_SECRET,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
args: [
|
||||
{
|
||||
type: 'remove_remote_schema',
|
||||
args: {
|
||||
name: remoteSchema.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
source: 'default',
|
||||
type: 'bulk',
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,31 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
});
|
||||
const { version } = require('./package.json');
|
||||
|
||||
const cspHeader = `
|
||||
default-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run;
|
||||
script-src 'self' 'unsafe-eval' cdn.segment.com js.stripe.com challenges.cloudflare.com googletagmanager.com;
|
||||
connect-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run discord.com api.segment.io api.segment.com cdn.segment.com nhost.zendesk.com;
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' blob: data: github.com avatars.githubusercontent.com s.gravatar.com *.nhost.run nhost.run;
|
||||
font-src 'self' data:;
|
||||
object-src 'none';
|
||||
base-uri 'self';
|
||||
form-action 'self';
|
||||
frame-ancestors 'none';
|
||||
frame-src 'self' js.stripe.com challenges.cloudflare.com;
|
||||
block-all-mixed-content;
|
||||
upgrade-insecure-requests;
|
||||
`;
|
||||
function getCspHeader() {
|
||||
switch (process.env.CSP_MODE) {
|
||||
case 'disabled':
|
||||
return null;
|
||||
case 'custom':
|
||||
return process.env.CSP_HEADER || null;
|
||||
case 'nhost':
|
||||
default:
|
||||
return [
|
||||
"default-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run",
|
||||
"script-src 'self' 'unsafe-eval' cdn.segment.com js.stripe.com challenges.cloudflare.com googletagmanager.com",
|
||||
"connect-src 'self' *.nhost.run wss://*.nhost.run nhost.run wss://nhost.run discord.com api.segment.io api.segment.com cdn.segment.com nhost.zendesk.com",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' blob: data: github.com avatars.githubusercontent.com s.gravatar.com *.nhost.run nhost.run",
|
||||
"font-src 'self' data:",
|
||||
"object-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
"frame-ancestors 'none'",
|
||||
"frame-src 'self' js.stripe.com challenges.cloudflare.com",
|
||||
"block-all-mixed-content",
|
||||
"upgrade-insecure-requests",
|
||||
].join('; ') + ';';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
reactStrictMode: false,
|
||||
@@ -34,13 +44,19 @@ module.exports = withBundleAnalyzer({
|
||||
dirs: ['src'],
|
||||
},
|
||||
async headers() {
|
||||
const cspHeader = getCspHeader();
|
||||
|
||||
if (!cspHeader) {
|
||||
return []; // No CSP headers
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
source: '/(.*)',
|
||||
source: '/:path*',
|
||||
headers: [
|
||||
{
|
||||
key: 'Content-Security-Policy',
|
||||
value: cspHeader.replace(/\s+/g, ' ').trim(),
|
||||
value: cspHeader,
|
||||
},
|
||||
{
|
||||
key: 'X-Frame-Options',
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
"analyze": "ANALYZE=true pnpm build --no-lint",
|
||||
"start": "next start",
|
||||
"lint": "next lint --max-warnings 0",
|
||||
"test": "vitest --run",
|
||||
"test": "pnpm lint && pnpm test:vitest",
|
||||
"test:vitest": "vitest --run",
|
||||
"test:watch": "vitest",
|
||||
"generate": "echo 'This needs to be fixed.'",
|
||||
"codegen": "DOTENV_CONFIG_PATH=./.env.local graphql-codegen -r dotenv/config --config graphql.config.yaml --errors-only",
|
||||
"codegen-graphite": "graphql-codegen --config graphite.graphql.config.yaml --errors-only",
|
||||
"codegen-hasura-api": "orval --config src/utils/hasura-api/orval.config.ts",
|
||||
"format": "prettier --write \"src/**/*.{js,ts,tsx,jsx,json,md}\" --plugin-search-dir=.",
|
||||
"storybook": "start-storybook -p 6006 -s public",
|
||||
"build-storybook": "build-storybook",
|
||||
"install-browsers": "pnpm playwright install && pnpm playwright install-deps",
|
||||
"e2e:tests": "pnpm playwright test --config=playwright.config.ts -x",
|
||||
"e2e": "pnpm e2e:tests --project=main",
|
||||
"e2e:local": "pnpm e2e:tests --project=local",
|
||||
@@ -110,7 +110,6 @@
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-intersection-observer": "^9.8.1",
|
||||
"react-is": "18.2.0",
|
||||
"react-loading-skeleton": "^2.2.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-merge-refs": "^3.0.2",
|
||||
"react-resizable-layout": "^0.7.2",
|
||||
@@ -135,21 +134,12 @@
|
||||
"@babel/core": "^7.24.3",
|
||||
"@eslint/js": "9.26.0",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
"@graphql-codegen/cli": "^6.0.0",
|
||||
"@graphql-codegen/typescript": "^3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@next/bundle-analyzer": "^12.3.4",
|
||||
"@playwright/test": "1.54.1",
|
||||
"@storybook/addon-actions": "^6.5.16",
|
||||
"@storybook/addon-essentials": "^6.5.16",
|
||||
"@storybook/addon-interactions": "^6.5.16",
|
||||
"@storybook/addon-links": "^6.5.16",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/builder-webpack5": "^6.5.16",
|
||||
"@storybook/manager-webpack5": "^6.5.16",
|
||||
"@storybook/react": "^7.6.17",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@tailwindcss/typography": "^0.5.12",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
@@ -159,7 +149,7 @@
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/node": "^16.18.93",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/pluralize": "^0.0.30",
|
||||
"@types/react": "18.2.73",
|
||||
"@types/react-dom": "^18.2.23",
|
||||
@@ -169,8 +159,8 @@
|
||||
"@types/validator": "^13.11.9",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@vitest/coverage-v8": "^0.32.4",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"audit-ci": "^6.6.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"babel-loader": "^8.3.0",
|
||||
@@ -194,24 +184,21 @@
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"lint-staged": "^15.2.2",
|
||||
"msw": "^1.3.5",
|
||||
"msw-storybook-addon": "^1.10.0",
|
||||
"msw": "^2.11.4",
|
||||
"node-fetch": "^3.3.2",
|
||||
"orval": "^7.11.2",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"react-date-fns-hooks": "^0.9.4",
|
||||
"require-from-string": "^2.0.2",
|
||||
"snake-case": "^3.0.4",
|
||||
"storybook-addon-next-router": "^4.0.2",
|
||||
"tailwindcss": "^3.4.12",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
||||
"vite": "^5.4.20",
|
||||
"vite": "^5.4.21",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^0.32.4"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -242,6 +229,9 @@
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild@<=0.24.2": ">=0.25.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ dotenv.config({ path: path.resolve(__dirname, '.env.test') });
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
maxFailures: 1,
|
||||
maxFailures: process.env.CI ? 3 : 1,
|
||||
timeout: 120 * 1000,
|
||||
expect: {
|
||||
timeout: 10000,
|
||||
},
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: 2,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
|
||||
10012
dashboard/pnpm-lock.yaml
generated
10012
dashboard/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -66,7 +66,6 @@ let
|
||||
"${submodule}/tsconfig.test.json"
|
||||
"${submodule}/vitest.config.ts"
|
||||
"${submodule}/vitest.global-setup.ts"
|
||||
(inDirectory "${submodule}/.storybook")
|
||||
(inDirectory "${submodule}/e2e")
|
||||
(inDirectory "${submodule}/public")
|
||||
(inDirectory "${submodule}/src")
|
||||
@@ -167,6 +166,12 @@ rec {
|
||||
'';
|
||||
};
|
||||
|
||||
packageWithDisabledCSP = package.overrideAttrs (oldAttrs: {
|
||||
configurePhase = oldAttrs.configurePhase + ''
|
||||
export CSP_MODE=disabled
|
||||
'';
|
||||
});
|
||||
|
||||
dockerImage = pkgs.runCommand "image-as-dir" { } ''
|
||||
${(nix2containerPkgs.nix2container.buildImage {
|
||||
inherit name created;
|
||||
@@ -176,7 +181,7 @@ rec {
|
||||
copyToRoot = pkgs.buildEnv {
|
||||
name = "image";
|
||||
paths = [
|
||||
package
|
||||
packageWithDisabledCSP
|
||||
(pkgs.writeTextFile {
|
||||
name = "tmp-file";
|
||||
text = ''
|
||||
@@ -213,5 +218,3 @@ rec {
|
||||
}).copyTo}/bin/copy-to dir:$out
|
||||
'';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -127,14 +127,14 @@ export default function AuthenticatedLayout({
|
||||
className="relative flex h-full flex-row overflow-hidden"
|
||||
ref={setMainNavContainer}
|
||||
>
|
||||
{mainNavPinned && isMdOrLarger && <PinnedMainNav />}
|
||||
{withMainNav && mainNavPinned && isMdOrLarger && <PinnedMainNav />}
|
||||
|
||||
<div
|
||||
className={cn('relative flex h-full w-full flex-row bg-accent', {
|
||||
'overflow-x-auto': mainNavPinned && isMdOrLarger && withMainNav,
|
||||
})}
|
||||
>
|
||||
{(!mainNavPinned || !isMdOrLarger) && (
|
||||
{withMainNav && (!mainNavPinned || !isMdOrLarger) && (
|
||||
<div className="flex h-full w-6 justify-center">
|
||||
<MainNav container={mainNavContainer} />
|
||||
</div>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import type { PropsWithoutRef } from 'react';
|
||||
|
||||
import type { ReadOnlyToggleProps } from './ReadOnlyToggle';
|
||||
import ReadOnlyToggle from './ReadOnlyToggle';
|
||||
|
||||
export default {
|
||||
title: 'Common Components / ReadOnlyToggle',
|
||||
component: ReadOnlyToggle,
|
||||
argTypes: {
|
||||
checked: {
|
||||
options: [null, true, false],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof ReadOnlyToggle>;
|
||||
|
||||
const Template: ComponentStory<typeof ReadOnlyToggle> = function Template(
|
||||
args: PropsWithoutRef<ReadOnlyToggleProps>,
|
||||
) {
|
||||
return <ReadOnlyToggle {...args} />;
|
||||
};
|
||||
|
||||
export const Null = Template.bind({});
|
||||
Null.args = {
|
||||
checked: null,
|
||||
};
|
||||
|
||||
export const True = Template.bind({});
|
||||
True.args = {
|
||||
checked: true,
|
||||
};
|
||||
|
||||
export const False = Template.bind({});
|
||||
False.args = {
|
||||
checked: false,
|
||||
};
|
||||
|
||||
export const CustomClasses = Template.bind({});
|
||||
CustomClasses.args = {
|
||||
checked: true,
|
||||
className: '!bg-red-500',
|
||||
slotProps: {
|
||||
label: {
|
||||
className: '!text-sm !text-white',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,125 +0,0 @@
|
||||
import { PlusCircleIcon } from '@/components/ui/v2/icons/PlusCircleIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import type { ButtonProps } from './Button';
|
||||
import Button from './Button';
|
||||
|
||||
export default {
|
||||
title: 'UI Library / Button',
|
||||
component: Button,
|
||||
argTypes: {
|
||||
variant: {
|
||||
options: ['contained', 'outlined', 'borderless'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
color: {
|
||||
options: ['primary', 'secondary', 'error'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
disabled: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
size: {
|
||||
options: ['small', 'medium', 'large'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
},
|
||||
} as Meta<typeof Button>;
|
||||
|
||||
const Template: StoryFn<ButtonProps> = function TemplateFunction(
|
||||
args: ButtonProps,
|
||||
) {
|
||||
return <Button {...args} />;
|
||||
};
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
Primary.args = {
|
||||
children: 'Button',
|
||||
color: 'primary',
|
||||
};
|
||||
|
||||
export const PrimaryOutlined = Template.bind({});
|
||||
PrimaryOutlined.args = {
|
||||
children: 'Button',
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
};
|
||||
|
||||
export const PrimaryBorderless = Template.bind({});
|
||||
PrimaryBorderless.args = {
|
||||
children: 'Button',
|
||||
variant: 'borderless',
|
||||
color: 'primary',
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
children: 'Button',
|
||||
color: 'secondary',
|
||||
};
|
||||
|
||||
export const SecondaryOutlined = Template.bind({});
|
||||
SecondaryOutlined.args = {
|
||||
children: 'Button',
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
};
|
||||
|
||||
export const SecondaryBorderless = Template.bind({});
|
||||
SecondaryBorderless.args = {
|
||||
children: 'Button',
|
||||
variant: 'borderless',
|
||||
color: 'secondary',
|
||||
};
|
||||
|
||||
export const Danger = Template.bind({});
|
||||
Danger.args = {
|
||||
children: 'Button',
|
||||
color: 'error',
|
||||
};
|
||||
|
||||
export const DangerOutlined = Template.bind({});
|
||||
DangerOutlined.args = {
|
||||
children: 'Button',
|
||||
variant: 'outlined',
|
||||
color: 'error',
|
||||
};
|
||||
|
||||
export const DangerBorderless = Template.bind({});
|
||||
DangerBorderless.args = {
|
||||
children: 'Button',
|
||||
variant: 'borderless',
|
||||
color: 'error',
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
children: 'Button',
|
||||
variant: 'contained',
|
||||
color: 'primary',
|
||||
size: 'small',
|
||||
};
|
||||
|
||||
export const Large = Template.bind({});
|
||||
Large.args = {
|
||||
children: 'Button',
|
||||
variant: 'contained',
|
||||
color: 'primary',
|
||||
size: 'large',
|
||||
};
|
||||
|
||||
export const WithStartIcon = Template.bind({});
|
||||
WithStartIcon.args = {
|
||||
children: 'Button',
|
||||
variant: 'contained',
|
||||
color: 'primary',
|
||||
startIcon: <PlusIcon />,
|
||||
};
|
||||
|
||||
export const WithEndIcon = Template.bind({});
|
||||
WithEndIcon.args = {
|
||||
children: 'Button',
|
||||
variant: 'contained',
|
||||
color: 'primary',
|
||||
endIcon: <PlusCircleIcon />,
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import { XIcon } from '@/components/ui/v2/icons/XIcon';
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import type { ChipProps } from './Chip';
|
||||
import Chip from './Chip';
|
||||
|
||||
export default {
|
||||
title: 'UI Library / Chip',
|
||||
component: Chip,
|
||||
argTypes: {
|
||||
variant: {
|
||||
options: ['contained', 'outlined'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
color: {
|
||||
options: ['primary', 'secondary', 'error', 'info'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
disabled: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
size: {
|
||||
options: ['small', 'medium'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof Chip>;
|
||||
|
||||
const Template: ComponentStory<typeof Chip> = function Template(
|
||||
args: ChipProps,
|
||||
) {
|
||||
return <Chip {...args} />;
|
||||
};
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
Primary.args = {
|
||||
label: 'Chip',
|
||||
color: 'primary',
|
||||
};
|
||||
|
||||
export const PrimaryOutlined = Template.bind({});
|
||||
PrimaryOutlined.args = {
|
||||
label: 'Chip',
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
label: 'Chip',
|
||||
color: 'secondary',
|
||||
};
|
||||
|
||||
export const SecondaryOutlined = Template.bind({});
|
||||
SecondaryOutlined.args = {
|
||||
label: 'Chip',
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
};
|
||||
|
||||
export const Danger = Template.bind({});
|
||||
Danger.args = {
|
||||
label: 'Chip',
|
||||
color: 'error',
|
||||
};
|
||||
|
||||
export const DangerOutlined = Template.bind({});
|
||||
DangerOutlined.args = {
|
||||
label: 'Chip',
|
||||
variant: 'outlined',
|
||||
color: 'error',
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
label: 'Chip',
|
||||
color: 'primary',
|
||||
size: 'small',
|
||||
};
|
||||
|
||||
export const WithDeleteIcon = Template.bind({});
|
||||
WithDeleteIcon.args = {
|
||||
label: 'Chip',
|
||||
color: 'primary',
|
||||
deleteIcon: <XIcon />,
|
||||
onDelete: () => {},
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import type { SelectProps } from './Select';
|
||||
import Select from './Select';
|
||||
|
||||
export default {
|
||||
title: 'UI Library / Select',
|
||||
component: Select,
|
||||
argTypes: {},
|
||||
} as Meta<typeof Select>;
|
||||
|
||||
const Template: StoryFn<SelectProps<any>> = function TemplateFunction(args) {
|
||||
return (
|
||||
<Select className="w-64" {...args}>
|
||||
<Option value="value1">Value 1</Option>
|
||||
<Option value="value2">Value 2</Option>
|
||||
<Option value="value3">Value 3</Option>
|
||||
<Option value="value4">Value 4</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
defaultValue: 'value1',
|
||||
};
|
||||
|
||||
export const WithLabel = Template.bind({});
|
||||
WithLabel.args = {
|
||||
label: 'Label',
|
||||
};
|
||||
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
label: 'Label',
|
||||
disabled: true,
|
||||
defaultValue: 'value1',
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import type { SwitchProps } from './Switch';
|
||||
import Switch from './Switch';
|
||||
|
||||
export default {
|
||||
title: 'UI Library / Switch',
|
||||
component: Switch,
|
||||
argTypes: {},
|
||||
} as Meta<typeof Switch>;
|
||||
|
||||
const Template: StoryFn<SwitchProps> = function TemplateFunction(
|
||||
args: SwitchProps,
|
||||
) {
|
||||
return <Switch label="Accept Rules" {...args} />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {};
|
||||
|
||||
export const Checked = Template.bind({});
|
||||
Checked.args = {
|
||||
checked: true,
|
||||
};
|
||||
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
disabled: true,
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ApplicationPaused } from '@/features/orgs/projects/common/components/ApplicationPaused';
|
||||
import { ApplicationPausedBanner } from '@/features/orgs/projects/common/components/ApplicationPausedBanner';
|
||||
import { useRouter } from 'next/router';
|
||||
import { type PropsWithChildren } from 'react';
|
||||
|
||||
const baseProjectPageRoute = '/orgs/[orgSlug]/projects/[appSubdomain]/';
|
||||
const blockedPausedProjectPages = [
|
||||
'database',
|
||||
'database/browser/[dataSourceSlug]',
|
||||
'graphql',
|
||||
'graphql/remote-schemas',
|
||||
'graphql/remote-schemas/[remoteSchemaSlug]',
|
||||
'hasura',
|
||||
'users',
|
||||
'storage',
|
||||
'ai/auto-embeddings',
|
||||
'ai/assistants',
|
||||
'metrics',
|
||||
].map((page) => baseProjectPageRoute.concat(page));
|
||||
|
||||
function PausedProjectContent({ children }: PropsWithChildren) {
|
||||
const { route } = useRouter();
|
||||
|
||||
const isOnOverviewPage = route === '/orgs/[orgSlug]/projects/[appSubdomain]';
|
||||
|
||||
if (isOnOverviewPage) {
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto mt-5 flex max-w-7xl p-4 pb-0">
|
||||
<ApplicationPausedBanner
|
||||
alertClassName="flex-row"
|
||||
textContainerClassName="flex flex-col items-center justify-center text-left"
|
||||
wakeUpButtonClassName="w-fit self-center"
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// block these pages when the project is paused
|
||||
if (blockedPausedProjectPages.includes(route)) {
|
||||
return <ApplicationPaused />;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
export default PausedProjectContent;
|
||||
@@ -1,5 +1,8 @@
|
||||
import { mockApplication } from '@/tests/mocks';
|
||||
import { getProjectQuery } from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import {
|
||||
getProjectQuery,
|
||||
getProjectStateQuery,
|
||||
} from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import {
|
||||
createGraphqlMockResolver,
|
||||
@@ -29,7 +32,7 @@ function TestComponent() {
|
||||
);
|
||||
}
|
||||
|
||||
const server = setupServer(tokenQuery);
|
||||
const server = setupServer(tokenQuery, getProjectStateQuery());
|
||||
|
||||
const getUseRouterObject = (
|
||||
route: string = '/orgs/[orgSlug]/projects/[appSubdomain]',
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
import { type AuthenticatedLayoutProps } from '@/components/layout/AuthenticatedLayout';
|
||||
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { ApplicationPaused } from '@/features/orgs/projects/common/components/ApplicationPaused';
|
||||
import { ApplicationPausedBanner } from '@/features/orgs/projects/common/components/ApplicationPausedBanner';
|
||||
import { ApplicationProvisioning } from '@/features/orgs/projects/common/components/ApplicationProvisioning';
|
||||
import { ApplicationRestoring } from '@/features/orgs/projects/common/components/ApplicationRestoring';
|
||||
import { ApplicationUnknown } from '@/features/orgs/projects/common/components/ApplicationUnknown';
|
||||
import { ApplicationUnpausing } from '@/features/orgs/projects/common/components/ApplicationUnpausing';
|
||||
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useProjectWithState } from '@/features/orgs/projects/hooks/useProjectWithState';
|
||||
import { isEmptyValue } from '@/lib/utils';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { isEmptyValue, isNotEmptyValue } from '@/lib/utils';
|
||||
import { useAuth } from '@/providers/Auth';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { getConfigServerUrl, isPlatform as isPlatformFn } from '@/utils/env';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback, useEffect, useMemo, type ReactNode } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import ProjectViewWithState from './ProjectViewWithState';
|
||||
|
||||
const platFormOnlyPages = [
|
||||
'/orgs/[orgSlug]/projects/[appSubdomain]/deployments',
|
||||
@@ -56,115 +48,15 @@ function ProjectLayoutContent({
|
||||
children,
|
||||
mainContainerProps = {},
|
||||
}: ProjectLayoutContentProps) {
|
||||
const {
|
||||
route,
|
||||
query: { appSubdomain },
|
||||
push,
|
||||
} = useRouter();
|
||||
const { route, push } = useRouter();
|
||||
|
||||
const { state } = useAppState();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const { project, loading, error, projectNotFound } = useProjectWithState();
|
||||
const { project, loading, error, projectNotFound } = useProject();
|
||||
const { isAuthenticated, isLoading, isSigningOut } = useAuth();
|
||||
|
||||
const isUserLoggedIn = isAuthenticated && !isLoading && !isSigningOut;
|
||||
|
||||
const isOnOverviewPage = route === '/orgs/[orgSlug]/projects/[appSubdomain]';
|
||||
|
||||
const renderPausedProjectContent = useCallback(
|
||||
(_children: ReactNode) => {
|
||||
const baseProjectPageRoute = '/orgs/[orgSlug]/projects/[appSubdomain]/';
|
||||
const blockedPausedProjectPages = [
|
||||
'database',
|
||||
'database/browser/[dataSourceSlug]',
|
||||
'database/browser/[dataSourceSlug]/[schemaSlug]/[tableSlug]',
|
||||
'graphql',
|
||||
'graphql/remote-schemas',
|
||||
'graphql/remote-schemas/[remoteSchemaSlug]',
|
||||
'hasura',
|
||||
'users',
|
||||
'storage',
|
||||
'run',
|
||||
'ai/auto-embeddings',
|
||||
'ai/assistants',
|
||||
'metrics',
|
||||
].map((page) => baseProjectPageRoute.concat(page));
|
||||
|
||||
// show an alert box on top of the overview page with a wake up button
|
||||
if (isOnOverviewPage) {
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto mt-5 flex max-w-7xl p-4 pb-0">
|
||||
<ApplicationPausedBanner
|
||||
alertClassName="flex-row"
|
||||
textContainerClassName="flex flex-col items-center justify-center text-left"
|
||||
wakeUpButtonClassName="w-fit self-center"
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// block these pages when the project is paused
|
||||
if (blockedPausedProjectPages.includes(route)) {
|
||||
return <ApplicationPaused />;
|
||||
}
|
||||
|
||||
return _children;
|
||||
},
|
||||
[route, isOnOverviewPage, children],
|
||||
);
|
||||
|
||||
// Render application state based on the current state
|
||||
const projectPageContent = useMemo(() => {
|
||||
if (!appSubdomain || state === undefined) {
|
||||
return children;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case ApplicationStatus.Empty:
|
||||
case ApplicationStatus.Provisioning:
|
||||
return <ApplicationProvisioning />;
|
||||
case ApplicationStatus.Errored:
|
||||
if (isOnOverviewPage) {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full p-4">
|
||||
<Alert severity="error" className="mx-auto max-w-7xl">
|
||||
Error deploying the project most likely due to invalid
|
||||
configuration. Please review your project's configuration
|
||||
and logs for more information.
|
||||
</Alert>
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return children;
|
||||
case ApplicationStatus.Pausing:
|
||||
case ApplicationStatus.Paused:
|
||||
return renderPausedProjectContent(children);
|
||||
case ApplicationStatus.Unpausing:
|
||||
return <ApplicationUnpausing />;
|
||||
case ApplicationStatus.Restoring:
|
||||
return <ApplicationRestoring />;
|
||||
case ApplicationStatus.Updating:
|
||||
case ApplicationStatus.Live:
|
||||
case ApplicationStatus.Migrating:
|
||||
return children;
|
||||
default:
|
||||
return <ApplicationUnknown />;
|
||||
}
|
||||
}, [
|
||||
state,
|
||||
children,
|
||||
appSubdomain,
|
||||
isOnOverviewPage,
|
||||
renderPausedProjectContent,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isPlatformOnlyPage(route) ||
|
||||
@@ -183,13 +75,11 @@ function ProjectLayoutContent({
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle loading state
|
||||
if (loading) {
|
||||
return <LoadingScreen data-testid="projectLoadingIndicator" />;
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (error) {
|
||||
if (isNotEmptyValue(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -211,7 +101,7 @@ function ProjectLayoutContent({
|
||||
)}
|
||||
{...mainContainerProps}
|
||||
>
|
||||
{projectPageContent}
|
||||
<ProjectViewWithState>{children}</ProjectViewWithState>
|
||||
<NextSeo title={!isPlatform ? 'Local App' : project?.name} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import {
|
||||
getProjectQuery,
|
||||
getProjectStateQuery,
|
||||
} from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { queryClient, render, screen } from '@/tests/testUtils';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { vi } from 'vitest';
|
||||
import ProjectViewWithState from './ProjectViewWithState';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
useRouter: vi.fn(),
|
||||
push: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('next/router', () => ({
|
||||
useRouter: mocks.useRouter,
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
'@/features/orgs/projects/common/components/ApplicationProvisioning',
|
||||
() => ({
|
||||
ApplicationProvisioning: () => <div>Application Provisioning</div>,
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
'@/features/orgs/projects/common/components/ApplicationRestoring',
|
||||
() => ({
|
||||
ApplicationRestoring: () => (
|
||||
<div data-testid="appRestore">Application Restoring</div>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
'@/features/orgs/projects/common/components/ApplicationUnknown',
|
||||
() => ({
|
||||
ApplicationUnknown: () => (
|
||||
<div data-testid="appUnknown">Application Unknown</div>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
'@/features/orgs/projects/common/components/ApplicationUnpausing',
|
||||
() => ({
|
||||
ApplicationUnpausing: () => (
|
||||
<div data-testid="appUnpausing">Application Unpausing</div>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
'@/features/orgs/projects/common/components/ApplicationPausedBanner',
|
||||
() => ({
|
||||
ApplicationPausedBanner: () => (
|
||||
<div data-testid="appBanner">Application Banner</div>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const getUseRouterObject = (
|
||||
route: string = '/orgs/[orgSlug]/projects/[appSubdomain]',
|
||||
) => ({
|
||||
basePath: '',
|
||||
pathname: '/orgs/xyz/projects/test-project',
|
||||
route,
|
||||
asPath: '/orgs/xyz/projects/test-project',
|
||||
isLocaleDomain: false,
|
||||
isReady: true,
|
||||
isPreview: false,
|
||||
query: {
|
||||
orgSlug: 'xyz',
|
||||
appSubdomain: 'test-project',
|
||||
},
|
||||
push: mocks.push,
|
||||
replace: vi.fn(),
|
||||
reload: vi.fn(),
|
||||
back: vi.fn(),
|
||||
prefetch: vi.fn(),
|
||||
beforePopState: vi.fn(),
|
||||
events: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
},
|
||||
isFallback: false,
|
||||
});
|
||||
|
||||
function TestComponent() {
|
||||
return (
|
||||
<ProjectViewWithState>
|
||||
<h1>Application content</h1>
|
||||
</ProjectViewWithState>
|
||||
);
|
||||
}
|
||||
|
||||
const server = setupServer(tokenQuery);
|
||||
|
||||
describe('ProjectViewWithState', () => {
|
||||
beforeAll(() => {
|
||||
process.env.NEXT_PUBLIC_NHOST_PLATFORM = 'true';
|
||||
process.env.NEXT_PUBLIC_ENV = 'production';
|
||||
server.listen();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear();
|
||||
mocks.useRouter.mockRestore();
|
||||
mocks.push.mockRestore();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should render the nothing when the state is empty', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Empty }]));
|
||||
render(<TestComponent />);
|
||||
expect(screen.queryByText('Application content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application in provisioning state', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(
|
||||
getProjectStateQuery([{ stateId: ApplicationStatus.Provisioning }]),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
expect(
|
||||
await screen.findByText('Application Provisioning'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText('Application content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application in pausing state', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Pausing }]));
|
||||
render(<TestComponent />);
|
||||
expect(await screen.findByText('Application content')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Application Banner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application Unpausing application state', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(
|
||||
getProjectStateQuery([{ stateId: ApplicationStatus.Unpausing }]),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
expect(screen.queryByText('Application content')).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText('Application Unpausing'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application paused application state', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Paused }]));
|
||||
render(<TestComponent />);
|
||||
expect(await screen.findByText('Application content')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Application Banner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application when the state is updating', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Updating }]));
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(await screen.findByText('Application content')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Application Restoring')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unknown')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unpausing')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Banner')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application when the state is live', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Live }]));
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(await screen.findByText('Application content')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Application Restoring')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unknown')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unpausing')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Banner')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application when the state is migrating', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(
|
||||
getProjectStateQuery([{ stateId: ApplicationStatus.Migrating }]),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(await screen.findByText('Application content')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Application Restoring')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unknown')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unpausing')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Banner')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application in an error state', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Errored }]));
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(await screen.findByText('Application content')).toBeInTheDocument();
|
||||
|
||||
expect(await screen.findByText(/Error deploying/)).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Application Restoring')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unknown')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unpausing')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Banner')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application in an error state', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(
|
||||
getProjectStateQuery([{ stateId: ApplicationStatus.Restoring }]),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(
|
||||
await screen.findByText('Application Restoring'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText('Application content')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { ApplicationProvisioning } from '@/features/orgs/projects/common/components/ApplicationProvisioning';
|
||||
import { ApplicationRestoring } from '@/features/orgs/projects/common/components/ApplicationRestoring';
|
||||
import { ApplicationUnknown } from '@/features/orgs/projects/common/components/ApplicationUnknown';
|
||||
import { ApplicationUnpausing } from '@/features/orgs/projects/common/components/ApplicationUnpausing';
|
||||
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { useRouter } from 'next/router';
|
||||
import { type PropsWithChildren, useMemo } from 'react';
|
||||
|
||||
import PausedProjectContent from './PausedProjectContent';
|
||||
|
||||
function ProjectViewWithState({ children }: PropsWithChildren) {
|
||||
const {
|
||||
query: { appSubdomain },
|
||||
route,
|
||||
} = useRouter();
|
||||
|
||||
const { state } = useAppState();
|
||||
|
||||
const isOnOverviewPage = route === '/orgs/[orgSlug]/projects/[appSubdomain]';
|
||||
|
||||
const projectPageContent = useMemo(() => {
|
||||
if (!appSubdomain || state === undefined) {
|
||||
return children;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case ApplicationStatus.Empty:
|
||||
return null;
|
||||
case ApplicationStatus.Provisioning:
|
||||
return <ApplicationProvisioning />;
|
||||
case ApplicationStatus.Errored:
|
||||
if (isOnOverviewPage) {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full p-4">
|
||||
<Alert severity="error" className="mx-auto max-w-7xl">
|
||||
Error deploying the project most likely due to invalid
|
||||
configuration. Please review your project's configuration
|
||||
and logs for more information.
|
||||
</Alert>
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return children;
|
||||
case ApplicationStatus.Pausing:
|
||||
case ApplicationStatus.Paused:
|
||||
return <PausedProjectContent>{children}</PausedProjectContent>;
|
||||
case ApplicationStatus.Unpausing:
|
||||
return <ApplicationUnpausing />;
|
||||
case ApplicationStatus.Restoring:
|
||||
return <ApplicationRestoring />;
|
||||
case ApplicationStatus.Updating:
|
||||
case ApplicationStatus.Live:
|
||||
case ApplicationStatus.Migrating:
|
||||
return children;
|
||||
default:
|
||||
return <ApplicationUnknown />;
|
||||
}
|
||||
}, [state, children, appSubdomain, isOnOverviewPage]);
|
||||
|
||||
return projectPageContent;
|
||||
}
|
||||
|
||||
export default ProjectViewWithState;
|
||||
@@ -176,7 +176,7 @@ export default function AppleProviderSettings() {
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
docsLink="https://docs.nhost.io/products/auth/social/sign-in-apple"
|
||||
docsLink="https://docs.nhost.io/products/auth/providers/sign-in-apple"
|
||||
docsTitle="how to sign in users with Apple"
|
||||
icon={
|
||||
theme.palette.mode === 'dark'
|
||||
|
||||
@@ -141,7 +141,7 @@ export default function DiscordProviderSettings() {
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
docsLink="https://docs.nhost.io/products/auth/social/sign-in-discord"
|
||||
docsLink="https://docs.nhost.io/products/auth/providers/sign-in-discord"
|
||||
docsTitle="how to sign in users with Discord"
|
||||
icon="/assets/brands/discord.svg"
|
||||
switchId="enabled"
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function FacebookProviderSettings() {
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
docsLink="https://docs.nhost.io/products/auth/social/sign-in-facebook"
|
||||
docsLink="https://docs.nhost.io/products/auth/providers/sign-in-facebook"
|
||||
docsTitle="how to sign in users with Facebook"
|
||||
icon="/assets/brands/facebook.svg"
|
||||
switchId="enabled"
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function GitHubProviderSettings() {
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
docsLink="https://docs.nhost.io/products/auth/social/sign-in-github"
|
||||
docsLink="https://docs.nhost.io/products/auth/providers/sign-in-github"
|
||||
docsTitle="how to sign in users with GitHub"
|
||||
icon={
|
||||
theme.palette.mode === 'dark'
|
||||
|
||||
@@ -162,7 +162,7 @@ export default function GoogleProviderSettings() {
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
docsLink="https://docs.nhost.io/products/auth/social/sign-in-google"
|
||||
docsLink="https://docs.nhost.io/products/auth/providers/sign-in-google"
|
||||
docsTitle="how to sign in users with Google"
|
||||
icon="/assets/brands/google.svg"
|
||||
switchId="enabled"
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function LinkedInProviderSettings() {
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
docsLink="https://docs.nhost.io/products/auth/social/sign-in-linkedin"
|
||||
docsLink="https://docs.nhost.io/products/auth/providers/sign-in-linkedin"
|
||||
docsTitle="how to sign in users with LinkedIn"
|
||||
icon="/assets/brands/linkedin.svg"
|
||||
switchId="enabled"
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function SpotifyProviderSettings() {
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
docsLink="https://docs.nhost.io/products/auth/social/sign-in-spotify"
|
||||
docsLink="https://docs.nhost.io/products/auth/providers/sign-in-spotify"
|
||||
docsTitle="how to sign in users with Spotify"
|
||||
icon="/assets/brands/spotify.svg"
|
||||
switchId="enabled"
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function TwitchProviderSettings() {
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
docsLink="https://docs.nhost.io/products/auth/social/sign-in-twitch"
|
||||
docsLink="https://docs.nhost.io/products/auth/providers/sign-in-twitch"
|
||||
docsTitle="how to sign in users with Twitch"
|
||||
icon={
|
||||
theme.palette.mode === 'dark'
|
||||
|
||||
@@ -177,7 +177,7 @@ export default function WorkOsProviderSettings() {
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
docsLink="https://docs.nhost.io/products/auth/social/sign-in-workos"
|
||||
docsLink="https://docs.nhost.io/products/auth/providers/sign-in-workos"
|
||||
docsTitle="how to sign in users with WorkOS"
|
||||
icon="/assets/brands/workos.svg"
|
||||
switchId="enabled"
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
getNotFoundProjectStateQuery,
|
||||
getProjectStateQuery,
|
||||
} from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { queryClient, render, screen, waitFor } from '@/tests/testUtils';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { vi } from 'vitest';
|
||||
import useAppState from './useAppState';
|
||||
|
||||
function TestComponent() {
|
||||
const { state } = useAppState();
|
||||
|
||||
return <h1>State: {state}</h1>;
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
refetch: vi.fn(),
|
||||
useProjectWithState: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/orgs/projects/hooks/useProject', async () => ({
|
||||
useProject: () => ({ refetch: mocks.refetch }),
|
||||
}));
|
||||
|
||||
const server = setupServer(tokenQuery);
|
||||
|
||||
describe('useAppState', () => {
|
||||
beforeAll(() => {
|
||||
process.env.NEXT_PUBLIC_NHOST_PLATFORM = 'true';
|
||||
process.env.NEXT_PUBLIC_ENV = 'production';
|
||||
server.listen();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear();
|
||||
mocks.refetch.mockRestore();
|
||||
mocks.useProjectWithState.mockRestore();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should refetch the project, when the project is not found', async () => {
|
||||
server.use(getNotFoundProjectStateQuery);
|
||||
render(<TestComponent />);
|
||||
expect(await screen.findByText('State: 0')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(mocks.refetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not refetch the project if the state is empty', async () => {
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Empty }]));
|
||||
render(<TestComponent />);
|
||||
expect(await screen.findByText('State: 0')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(mocks.refetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should return empty state if the application state has not been filled yet', async () => {
|
||||
server.use(getProjectStateQuery([]));
|
||||
render(<TestComponent />);
|
||||
expect(await screen.findByText('State: 0')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(mocks.refetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should return the first state from the response', async () => {
|
||||
server.use(
|
||||
getProjectStateQuery([
|
||||
{ stateId: ApplicationStatus.Live },
|
||||
{ stateId: ApplicationStatus.Empty },
|
||||
]),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
expect(await screen.findByText('State: 5')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(mocks.refetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { useProjectWithState } from '@/features/orgs/projects/hooks/useProjectWithState';
|
||||
import { isNotEmptyValue } from '@/lib/utils';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* This hook returns the current application state. If the application state
|
||||
@@ -7,27 +10,19 @@ import { ApplicationStatus } from '@/types/application';
|
||||
*/
|
||||
export default function useAppState(): {
|
||||
state: ApplicationStatus;
|
||||
message?: string | null;
|
||||
} {
|
||||
const { project } = useProjectWithState();
|
||||
const noApplication = !project;
|
||||
const { project, projectNotFound } = useProjectWithState();
|
||||
const { refetch } = useProject();
|
||||
|
||||
if (noApplication) {
|
||||
return { state: ApplicationStatus.Empty };
|
||||
}
|
||||
|
||||
const emptyApplicationStates = !project.appStates;
|
||||
|
||||
if (noApplication || emptyApplicationStates) {
|
||||
return { state: ApplicationStatus.Empty };
|
||||
}
|
||||
|
||||
if (project.appStates?.length === 0) {
|
||||
return { state: ApplicationStatus.Empty };
|
||||
}
|
||||
useEffect(() => {
|
||||
if (projectNotFound) {
|
||||
refetch();
|
||||
}
|
||||
}, [projectNotFound, refetch]);
|
||||
|
||||
return {
|
||||
state: project.appStates[0].stateId,
|
||||
message: project.appStates[0].message,
|
||||
state: isNotEmptyValue(project?.appStates?.[0])
|
||||
? project.appStates[0].stateId
|
||||
: ApplicationStatus.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,9 +74,6 @@ export default function useCheckProvisioning() {
|
||||
createdAt: data.app.appStates[0].createdAt,
|
||||
});
|
||||
stopPolling();
|
||||
// Will update the cache and update with the new application state
|
||||
// which will trigger the correct application component
|
||||
// under `src\components\applications\App.tsx`
|
||||
memoizedUpdateCache();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import hasuraMetadataQuery from '@/tests/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/tests/msw/mocks/rest/tableQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type { ColumnAutocompleteProps } from './ColumnAutocomplete';
|
||||
import ColumnAutocomplete from './ColumnAutocomplete';
|
||||
|
||||
export default {
|
||||
title: 'Data Browser / ColumnAutocomplete',
|
||||
component: ColumnAutocomplete,
|
||||
parameters: {
|
||||
docs: {
|
||||
source: {
|
||||
type: 'code',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof ColumnAutocomplete>;
|
||||
|
||||
const defaultParameters = {
|
||||
nextRouter: {
|
||||
path: '/[workspaceSlug]/[appSlug]/database/browser/[dataSourceSlug]/[schemaSlug]/[tableSlug]',
|
||||
asPath: '/workspace/app/database/browser/default/public/users',
|
||||
query: {
|
||||
workspaceSlug: 'workspace',
|
||||
appSlug: 'app',
|
||||
dataSourceSlug: 'default',
|
||||
schemaSlug: 'public',
|
||||
tableSlug: 'books',
|
||||
},
|
||||
},
|
||||
msw: {
|
||||
handlers: [tokenQuery, tableQuery, hasuraMetadataQuery],
|
||||
},
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof ColumnAutocomplete> = function Template(
|
||||
args: ColumnAutocompleteProps,
|
||||
) {
|
||||
const [submittedValues, setSubmittedValues] = useState<string>('');
|
||||
|
||||
const form = useForm<{ firstReference: string; secondReference: string }>({
|
||||
defaultValues: {
|
||||
firstReference: null as any,
|
||||
secondReference: null as any,
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit(values: {
|
||||
firstReference: string;
|
||||
secondReference: string;
|
||||
}) {
|
||||
setSubmittedValues(JSON.stringify(values, null, 2));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
|
||||
<ColumnAutocomplete
|
||||
{...args}
|
||||
onChange={(newValue) =>
|
||||
form.setValue('firstReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
onInitialized={(newValue) => {
|
||||
form.setValue('firstReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ColumnAutocomplete
|
||||
{...args}
|
||||
onChange={(newValue) =>
|
||||
form.setValue('secondReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
onInitialized={(newValue) => {
|
||||
form.setValue('secondReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="justify-self-start">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
|
||||
<Text component="pre" className="!font-mono !text-gray-700">
|
||||
{submittedValues || 'The form has not been submitted yet.'}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Basic = Template.bind({});
|
||||
Basic.args = {
|
||||
schema: 'public',
|
||||
table: 'books',
|
||||
};
|
||||
Basic.parameters = defaultParameters;
|
||||
|
||||
export const DefaultValue = Template.bind({});
|
||||
DefaultValue.args = {
|
||||
schema: 'public',
|
||||
table: 'books',
|
||||
value: 'author.id',
|
||||
};
|
||||
DefaultValue.parameters = defaultParameters;
|
||||
|
||||
export const DisabledRelationships = Template.bind({});
|
||||
DisabledRelationships.args = {
|
||||
schema: 'public',
|
||||
table: 'books',
|
||||
disableRelationships: true,
|
||||
};
|
||||
DisabledRelationships.parameters = defaultParameters;
|
||||
@@ -127,6 +127,10 @@ describe('RowPermissionsSection', () => {
|
||||
process.env.NEXT_PUBLIC_ENV = 'dev';
|
||||
server.listen();
|
||||
});
|
||||
beforeEach(() => {
|
||||
server.restoreHandlers();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import type { RuleGroup } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import permissionVariablesQuery from '@/tests/msw/mocks/graphql/permissionVariablesQuery';
|
||||
import hasuraMetadataQuery from '@/tests/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/tests/msw/mocks/rest/tableQuery';
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type { RuleGroupEditorProps } from './RuleGroupEditor';
|
||||
import RuleGroupEditor from './RuleGroupEditor';
|
||||
|
||||
export default {
|
||||
title: 'Data Browser / RuleGroupEditor',
|
||||
component: RuleGroupEditor,
|
||||
parameters: {
|
||||
docs: {
|
||||
source: {
|
||||
type: 'code',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof RuleGroupEditor>;
|
||||
|
||||
const defaultParameters = {
|
||||
nextRouter: {
|
||||
path: '/[workspaceSlug]/[appSlug]/database/browser/[dataSourceSlug]/[schemaSlug]/[tableSlug]',
|
||||
asPath: '/workspace/app/database/browser/default/public/users',
|
||||
query: {
|
||||
workspaceSlug: 'workspace',
|
||||
appSlug: 'app',
|
||||
dataSourceSlug: 'default',
|
||||
schemaSlug: 'public',
|
||||
tableSlug: 'books',
|
||||
},
|
||||
},
|
||||
msw: {
|
||||
handlers: [tableQuery, hasuraMetadataQuery, permissionVariablesQuery],
|
||||
},
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof RuleGroupEditor> = function Template(
|
||||
args: RuleGroupEditorProps,
|
||||
) {
|
||||
const [submittedValues, setSubmittedValues] = useState<string>();
|
||||
|
||||
const form = useForm<{ ruleGroupEditor: RuleGroup }>({
|
||||
defaultValues: {
|
||||
ruleGroupEditor: {
|
||||
operator: '_and',
|
||||
rules: [{ column: '', operator: '_eq', value: '' }],
|
||||
groups: [],
|
||||
},
|
||||
},
|
||||
reValidateMode: 'onSubmit',
|
||||
});
|
||||
|
||||
function handleSubmit(values: { ruleGroupEditor: RuleGroup }) {
|
||||
setSubmittedValues(JSON.stringify(values, null, 2));
|
||||
}
|
||||
|
||||
// note: Storybook passes `onRemove` as a prop, but we don't want to use it
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
|
||||
<RuleGroupEditor
|
||||
{...args}
|
||||
schema="public"
|
||||
table="books"
|
||||
name="ruleGroupEditor"
|
||||
/>
|
||||
|
||||
<Button type="submit" className="justify-self-start">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
|
||||
<Text component="pre" className="!font-mono !text-gray-700">
|
||||
{submittedValues || 'The form has not been submitted yet.'}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {};
|
||||
Default.parameters = defaultParameters;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { rest } from 'msw';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import type { ManagePermissionOptions } from './managePermission';
|
||||
import managePermission from './managePermission';
|
||||
@@ -12,9 +12,7 @@ const defaultParameters: ManagePermissionOptions = {
|
||||
};
|
||||
|
||||
const server = setupServer(
|
||||
rest.post('http://localhost:1337/v1/metadata', (_req, res, ctx) =>
|
||||
res(ctx.json({})),
|
||||
),
|
||||
http.post('http://localhost:1337/v1/metadata', () => HttpResponse.json({})),
|
||||
);
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
|
||||
@@ -3,7 +3,7 @@ import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/gen
|
||||
import { useDatabaseQuery } from '@/features/orgs/projects/database/dataGrid/hooks/useDatabaseQuery';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import { getHasuraAdminSecret, getHasuraMigrationsApiUrl } from '@/utils/env';
|
||||
import { parseIdentifiersFromSQL } from '@/utils/sql';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
@@ -53,7 +53,10 @@ export default function useRunSQL(
|
||||
isCascade: boolean,
|
||||
) => {
|
||||
try {
|
||||
const migrationApiResponse = await fetch(`${appUrl}/apis/migrate`, {
|
||||
const url = isPlatform
|
||||
? `${appUrl}/apis/migrate`
|
||||
: getHasuraMigrationsApiUrl();
|
||||
const migrationApiResponse = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'x-hasura-admin-secret': adminSecret },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HasuraMetadata } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import { rest } from 'msw';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import prepareTrackForeignKeyRelationsMetadata from './prepareTrackForeignKeyRelationsMetadata';
|
||||
|
||||
@@ -28,11 +28,8 @@ const testMetadataResponse: { metadata: HasuraMetadata } = {
|
||||
};
|
||||
|
||||
const metadataHandlers = [
|
||||
rest.post(`${APP_URL}/v1/metadata`, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json<{ metadata: HasuraMetadata }>(testMetadataResponse),
|
||||
),
|
||||
http.post(`${APP_URL}/v1/metadata`, () =>
|
||||
HttpResponse.json(testMetadataResponse),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -131,56 +128,53 @@ test('should only prepare a one-to-one relationship if the table does not have a
|
||||
|
||||
test('should drop existing relationships and prepare a new one-to-many relationship', async () => {
|
||||
server.use(
|
||||
rest.post(`${APP_URL}/v1/metadata`, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json<{ metadata: HasuraMetadata }>({
|
||||
...testMetadataResponse,
|
||||
metadata: {
|
||||
...testMetadataResponse.metadata,
|
||||
sources: [
|
||||
{
|
||||
...testMetadataResponse.metadata.sources[0],
|
||||
tables: [
|
||||
{
|
||||
...testMetadataResponse.metadata.sources[0].tables[0],
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'author',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'author_id',
|
||||
},
|
||||
http.post(`${APP_URL}/v1/metadata`, () =>
|
||||
HttpResponse.json({
|
||||
...testMetadataResponse,
|
||||
metadata: {
|
||||
...testMetadataResponse.metadata,
|
||||
sources: [
|
||||
{
|
||||
...testMetadataResponse.metadata.sources[0],
|
||||
tables: [
|
||||
{
|
||||
...testMetadataResponse.metadata.sources[0].tables[0],
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'author',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'author_id',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'authors',
|
||||
schema: 'public',
|
||||
},
|
||||
configuration: {},
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'books',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'author_id',
|
||||
table: {
|
||||
name: 'books',
|
||||
schema: 'public',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'authors',
|
||||
schema: 'public',
|
||||
},
|
||||
configuration: {},
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'books',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'author_id',
|
||||
table: {
|
||||
name: 'books',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
object_relationships: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
object_relationships: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { render, screen, TestUserEvent } from '@/tests/testUtils';
|
||||
import { vi } from 'vitest';
|
||||
import DatabasePiTRSettings from './DatabasePiTRSettings';
|
||||
|
||||
import { getOrganizations } from '@/tests/msw/mocks/graphql/getOrganizationQuery';
|
||||
import { getProjectQuery } from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { setupServer } from 'msw/node';
|
||||
@@ -75,7 +76,7 @@ vi.mock('@/features/orgs/components/common/TransferProjectDialog', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
const server = setupServer(tokenQuery);
|
||||
const server = setupServer(tokenQuery, getOrganizations);
|
||||
|
||||
describe('DatabasePiTRSettings', () => {
|
||||
beforeAll(() => {
|
||||
|
||||
@@ -52,11 +52,11 @@ function UpgradeNotification({ description }: Props) {
|
||||
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
<OpenTransferDialogButton onClick={handleTransferDialogOpen} />
|
||||
<TransferProjectDialog
|
||||
open={transferProjectDialogOpen}
|
||||
setOpen={setTransferProjectDialogOpen}
|
||||
/>
|
||||
</Text>
|
||||
<TransferProjectDialog
|
||||
open={transferProjectDialogOpen}
|
||||
setOpen={setTransferProjectDialogOpen}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,17 @@ const mockServices = [
|
||||
'job-backup',
|
||||
];
|
||||
|
||||
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
|
||||
value: vi.fn(() => ({
|
||||
width: 100,
|
||||
height: 40,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 40,
|
||||
right: 100,
|
||||
})),
|
||||
});
|
||||
|
||||
vi.mock('@/features/orgs/projects/hooks/useProject', async () => ({
|
||||
useProject: () => ({ project: mockApplication }),
|
||||
}));
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { render, screen, waitFor } from '@/tests/testUtils';
|
||||
import { graphql } from 'msw';
|
||||
import { HttpResponse, graphql } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { beforeAll, expect, test, vi } from 'vitest';
|
||||
import HasuraCorsDomainSettings from './HasuraCorsDomainSettings';
|
||||
|
||||
const server = setupServer(
|
||||
tokenQuery,
|
||||
graphql.query('GetHasuraSettings', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
graphql.query('GetHasuraSettings', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
config: {
|
||||
id: 'HasuraSettings',
|
||||
__typename: 'HasuraSettings',
|
||||
@@ -29,8 +29,8 @@ const server = setupServer(
|
||||
resources: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -62,9 +62,9 @@ describe('HasuraCorsDomainSettings', () => {
|
||||
|
||||
test('should enable switch by default when CORS domain is set to one or more domains', async () => {
|
||||
server.use(
|
||||
graphql.query('GetHasuraSettings', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
graphql.query('GetHasuraSettings', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
config: {
|
||||
id: 'HasuraSettings',
|
||||
__typename: 'HasuraSettings',
|
||||
@@ -84,8 +84,8 @@ describe('HasuraCorsDomainSettings', () => {
|
||||
resources: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { localApplication } from '@/features/orgs/utils/local-dashboard';
|
||||
import { isEmptyValue } from '@/lib/utils';
|
||||
import { useAuth } from '@/providers/Auth';
|
||||
import { useNhostClient } from '@/providers/nhost';
|
||||
import {
|
||||
@@ -18,6 +19,7 @@ export interface UseProjectReturnType {
|
||||
loading?: boolean;
|
||||
error?: Error | null;
|
||||
refetch: (variables?: any) => Promise<any>;
|
||||
projectNotFound: boolean;
|
||||
}
|
||||
|
||||
export default function useProject(): UseProjectReturnType {
|
||||
@@ -39,13 +41,14 @@ export default function useProject(): UseProjectReturnType {
|
||||
[isPlatform, isAuthenticated, isAuthLoading, appSubdomain, isRouterReady],
|
||||
);
|
||||
|
||||
const { data, isLoading, refetch, error } = useQuery(
|
||||
const { data, isLoading, refetch, error, isFetched } = useQuery(
|
||||
['project', appSubdomain as string],
|
||||
async () => {
|
||||
const response = await nhost.graphql.request<{
|
||||
apps: ProjectFragment[];
|
||||
}>(GetProjectDocument, { subdomain: (appSubdomain as string) || '' });
|
||||
return response.body;
|
||||
|
||||
return response?.body.data;
|
||||
},
|
||||
{
|
||||
enabled: shouldFetchProject,
|
||||
@@ -54,10 +57,11 @@ export default function useProject(): UseProjectReturnType {
|
||||
|
||||
if (isPlatform) {
|
||||
return {
|
||||
project: data?.data?.apps?.[0] || null,
|
||||
project: data?.apps?.[0] || null,
|
||||
loading: isLoading && shouldFetchProject,
|
||||
error: Array.isArray(error || {}) ? error?.[0] : error,
|
||||
refetch,
|
||||
projectNotFound: isFetched && !isLoading && isEmptyValue(data?.apps),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,5 +70,6 @@ export default function useProject(): UseProjectReturnType {
|
||||
loading: false,
|
||||
error: null,
|
||||
refetch: () => Promise.resolve(),
|
||||
projectNotFound: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ describe('useProjectLogs - Subscription Creation & Cleanup', () => {
|
||||
loading: false,
|
||||
error: undefined,
|
||||
refetch: vi.fn(),
|
||||
projectNotFound: false,
|
||||
});
|
||||
|
||||
// Mock subscribeToMore to return an unsubscribe function
|
||||
@@ -133,6 +134,7 @@ describe('useProjectLogs - Subscription Creation & Cleanup', () => {
|
||||
loading: true,
|
||||
error: undefined,
|
||||
refetch: vi.fn(),
|
||||
projectNotFound: false,
|
||||
});
|
||||
|
||||
renderHook(() => useProjectLogs(defaultProps));
|
||||
@@ -146,6 +148,7 @@ describe('useProjectLogs - Subscription Creation & Cleanup', () => {
|
||||
loading: false,
|
||||
error: undefined,
|
||||
refetch: vi.fn(),
|
||||
projectNotFound: false,
|
||||
});
|
||||
|
||||
renderHook(() => useProjectLogs(defaultProps));
|
||||
|
||||
@@ -20,6 +20,17 @@ const mockServices = [
|
||||
'job-backup',
|
||||
];
|
||||
|
||||
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
|
||||
value: vi.fn(() => ({
|
||||
width: 100,
|
||||
height: 40,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 40,
|
||||
right: 100,
|
||||
})),
|
||||
});
|
||||
|
||||
vi.mock('@/features/orgs/projects/hooks/useProject', async () => ({
|
||||
useProject: () => ({ project: mockApplication }),
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mockApplication, mockOrganization } from '@/tests/mocks';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { queryClient, render, screen } from '@/tests/testUtils';
|
||||
import { rest } from 'msw';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { afterAll, beforeAll, vi } from 'vitest';
|
||||
import OverviewDeployments from './OverviewDeployments';
|
||||
@@ -36,8 +36,9 @@ vi.mock('next/router', () => ({
|
||||
|
||||
const server = setupServer(
|
||||
tokenQuery,
|
||||
rest.get('https://local.graphql.local.nhost.run/v1', (_req, res, ctx) =>
|
||||
res(ctx.status(200)),
|
||||
http.get(
|
||||
'https://local.graphql.local.nhost.run/v1',
|
||||
() => new HttpResponse(null, { status: 200 }),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -49,8 +50,9 @@ beforeAll(() => {
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers(
|
||||
rest.get('https://local.graphql.local.nhost.run/v1', (_req, res, ctx) =>
|
||||
res(ctx.status(200)),
|
||||
http.get(
|
||||
'https://local.graphql.local.nhost.run/v1',
|
||||
() => new HttpResponse(null, { status: 200 }),
|
||||
),
|
||||
);
|
||||
queryClient.clear();
|
||||
@@ -63,37 +65,31 @@ afterAll(() => {
|
||||
|
||||
test('should render an empty state when GitHub is not connected', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
http.post(
|
||||
'https://local.graphql.local.nhost.run/v1',
|
||||
async (req, res, ctx) => {
|
||||
const { operationName } = await req.json();
|
||||
async ({ request }) => {
|
||||
const { operationName } = (await request.json()) as any;
|
||||
if (operationName === 'getProject') {
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication, githubRepository: null }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication, githubRepository: null }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (operationName === 'getOrganization') {
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
deployments: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
deployments: [],
|
||||
},
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -107,32 +103,28 @@ test('should render an empty state when GitHub is not connected', async () => {
|
||||
});
|
||||
test('should render an empty state when GitHub is connected, but there are no deployments', async () => {
|
||||
server.use(
|
||||
rest.post(
|
||||
http.post(
|
||||
'https://local.graphql.local.nhost.run/v1',
|
||||
async (_req, res, ctx) => {
|
||||
const { operationName } = await _req.json();
|
||||
async ({ request }) => {
|
||||
const { operationName } = (await request.json()) as any;
|
||||
|
||||
if (operationName === 'getProject') {
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (operationName === 'getOrganization') {
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return res(ctx.json({ data: { deployments: [] } }));
|
||||
return HttpResponse.json({ data: { deployments: [] } });
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -155,52 +147,46 @@ test('should render an empty state when GitHub is connected, but there are no de
|
||||
test('should render a list of deployments', async () => {
|
||||
server.use(
|
||||
tokenQuery,
|
||||
rest.post(
|
||||
http.post(
|
||||
'https://local.graphql.local.nhost.run/v1',
|
||||
async (_req, res, ctx) => {
|
||||
const { operationName } = await _req.json();
|
||||
async ({ request }) => {
|
||||
const { operationName } = (await request.json()) as any;
|
||||
|
||||
if (operationName === 'ScheduledOrPendingDeploymentsSub') {
|
||||
return res(ctx.json({ data: { deployments: [] } }));
|
||||
return HttpResponse.json({ data: { deployments: [] } });
|
||||
}
|
||||
|
||||
if (operationName === 'getProject') {
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication }],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (operationName === 'getOrganization') {
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
deployments: [
|
||||
{
|
||||
id: '1',
|
||||
commitSHA: 'abc123',
|
||||
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
|
||||
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
|
||||
deploymentStatus: 'DEPLOYED',
|
||||
commitUserName: 'test.user',
|
||||
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
|
||||
commitMessage: 'Test commit message',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
deployments: [
|
||||
{
|
||||
id: '1',
|
||||
commitSHA: 'abc123',
|
||||
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
|
||||
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
|
||||
deploymentStatus: 'DEPLOYED',
|
||||
commitUserName: 'test.user',
|
||||
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
|
||||
commitMessage: 'Test commit message',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -227,69 +213,61 @@ test('should render a list of deployments', async () => {
|
||||
test('should disable redeployments if a deployment is already in progress', async () => {
|
||||
server.use(
|
||||
tokenQuery,
|
||||
rest.post(
|
||||
http.post(
|
||||
'https://local.graphql.local.nhost.run/v1',
|
||||
async (req, res, ctx) => {
|
||||
const { operationName } = await req.json();
|
||||
async ({ request }) => {
|
||||
const { operationName } = (await request.json()) as any;
|
||||
|
||||
if (operationName === 'ScheduledOrPendingDeploymentsSub') {
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
deployments: [
|
||||
{
|
||||
id: '2',
|
||||
commitSHA: 'abc234',
|
||||
deploymentStartedAt: '2021-08-02T00:00:00.000Z',
|
||||
deploymentEndedAt: null,
|
||||
deploymentStatus: 'PENDING',
|
||||
commitUserName: 'test.user',
|
||||
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
|
||||
commitMessage: 'Test commit message',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (operationName === 'getProject') {
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operationName === 'getOrganization') {
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.json({
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
deployments: [
|
||||
{
|
||||
id: '1',
|
||||
commitSHA: 'abc123',
|
||||
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
|
||||
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
|
||||
deploymentStatus: 'DEPLOYED',
|
||||
id: '2',
|
||||
commitSHA: 'abc234',
|
||||
deploymentStartedAt: '2021-08-02T00:00:00.000Z',
|
||||
deploymentEndedAt: null,
|
||||
deploymentStatus: 'PENDING',
|
||||
commitUserName: 'test.user',
|
||||
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
|
||||
commitMessage: 'Test commit message',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (operationName === 'getProject') {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication }],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (operationName === 'getOrganization') {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
deployments: [
|
||||
{
|
||||
id: '1',
|
||||
commitSHA: 'abc123',
|
||||
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
|
||||
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
|
||||
deploymentStatus: 'DEPLOYED',
|
||||
commitUserName: 'test.user',
|
||||
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
|
||||
commitMessage: 'Test commit message',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
165
dashboard/src/pages/api/support/create-ticket.ts
Normal file
165
dashboard/src/pages/api/support/create-ticket.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { nhostRoutesClient } from '@/utils/nhost';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export type CreateTicketRequest = {
|
||||
project: string;
|
||||
services: Array<{ label: string; value: string }>;
|
||||
priority: string;
|
||||
subject: string;
|
||||
description: string;
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
};
|
||||
|
||||
export type CreateTicketResponse = {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type GetProjectResponse = {
|
||||
apps: Array<{
|
||||
id: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<CreateTicketResponse>,
|
||||
) {
|
||||
if (req.method !== 'POST') {
|
||||
return res
|
||||
.status(405)
|
||||
.json({ success: false, error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
project,
|
||||
services,
|
||||
priority,
|
||||
subject,
|
||||
description,
|
||||
userName,
|
||||
userEmail,
|
||||
} = req.body as CreateTicketRequest;
|
||||
|
||||
// Validate required environment variables
|
||||
if (
|
||||
!process.env.NEXT_ZENDESK_USER_EMAIL ||
|
||||
!process.env.NEXT_ZENDESK_API_KEY ||
|
||||
!process.env.NEXT_ZENDESK_URL
|
||||
) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Zendesk configuration is missing',
|
||||
});
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
!project ||
|
||||
!services ||
|
||||
!priority ||
|
||||
!subject ||
|
||||
!description ||
|
||||
!userName ||
|
||||
!userEmail
|
||||
) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Missing required fields',
|
||||
});
|
||||
}
|
||||
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
|
||||
try {
|
||||
// we use this to verify the owner of the JWT token has access to the project
|
||||
const resp = await nhostRoutesClient.graphql.request<GetProjectResponse>(
|
||||
{
|
||||
query: `query GetProject($subdomain: String!){
|
||||
apps(where: {subdomain: {_eq: $subdomain}}) {
|
||||
id
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
subdomain: project,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (resp.body.data?.apps.length !== 1) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid project subdomain',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid project subdomain',
|
||||
});
|
||||
}
|
||||
|
||||
const auth = btoa(
|
||||
`${process.env.NEXT_ZENDESK_USER_EMAIL}/token:${process.env.NEXT_ZENDESK_API_KEY}`,
|
||||
);
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_ZENDESK_URL}/api/v2/requests.json`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${auth}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
request: {
|
||||
subject,
|
||||
comment: {
|
||||
body: description,
|
||||
},
|
||||
priority,
|
||||
requester: {
|
||||
name: userName,
|
||||
email: userEmail,
|
||||
},
|
||||
custom_fields: [
|
||||
// these custom field IDs come from zendesk
|
||||
{
|
||||
id: 19502784542098,
|
||||
value: project,
|
||||
},
|
||||
{
|
||||
id: 19922709880978,
|
||||
value: services.map((service) => service.value?.toLowerCase()),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Zendesk API error:', errorText);
|
||||
return res.status(response.status).json({
|
||||
success: false,
|
||||
error: `Failed to create ticket: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error creating ticket:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'An unexpected error occurred',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,9 @@ function SupportPage() {
|
||||
return (
|
||||
<Box className="h-full overflow-auto pb-4">
|
||||
<Box className="flex w-full justify-start border-b-1 px-4 py-3">
|
||||
<Logo className="w-6 cursor-pointer" />
|
||||
<Link href="https://app.nhost.io" rel="noopener noreferrer">
|
||||
<Logo className="w-6" />
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
|
||||
@@ -5,11 +5,11 @@ import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { EnvelopeIcon } from '@/components/ui/v2/icons/EnvelopeIcon';
|
||||
import { Input, inputClasses } from '@/components/ui/v2/Input';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { useAccessToken } from '@/hooks/useAccessToken';
|
||||
import { useUserData } from '@/hooks/useUserData';
|
||||
import {
|
||||
useGetOrganizationsQuery,
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { styled } from '@mui/material';
|
||||
import { Mail } from 'lucide-react';
|
||||
import { type ReactElement } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
@@ -27,7 +28,7 @@ type Organization = Omit<
|
||||
>;
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
organization: Yup.string().label('Organization'),
|
||||
organization: Yup.string().label('Organization').required(),
|
||||
project: Yup.string().label('Project').required(),
|
||||
services: Yup.array()
|
||||
.of(Yup.object({ label: Yup.string(), value: Yup.string() }))
|
||||
@@ -36,7 +37,6 @@ const validationSchema = Yup.object({
|
||||
priority: Yup.string().label('Priority').required(),
|
||||
subject: Yup.string().label('Subject').required(),
|
||||
description: Yup.string().label('Description').required(),
|
||||
ccs: Yup.string().label('CCs').optional(),
|
||||
});
|
||||
|
||||
export type CreateTicketFormValues = Yup.InferType<typeof validationSchema>;
|
||||
@@ -58,7 +58,6 @@ function TicketPage() {
|
||||
priority: '',
|
||||
subject: '',
|
||||
description: '',
|
||||
ccs: '',
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
@@ -71,6 +70,7 @@ function TicketPage() {
|
||||
|
||||
const selectedOrganization = watch('organization');
|
||||
const user = useUserData();
|
||||
const token = useAccessToken();
|
||||
|
||||
const { data: organizationsData } = useGetOrganizationsQuery({
|
||||
variables: {
|
||||
@@ -91,56 +91,32 @@ function TicketPage() {
|
||||
};
|
||||
|
||||
const handleSubmit = async (formValues: CreateTicketFormValues) => {
|
||||
const { project, services, priority, subject, description, ccs } =
|
||||
formValues;
|
||||
|
||||
const auth = btoa(
|
||||
`${process.env.NEXT_PUBLIC_ZENDESK_USER_EMAIL}/token:${process.env.NEXT_PUBLIC_ZENDESK_API_KEY}`,
|
||||
);
|
||||
const emails = ccs
|
||||
?.replace(/ /g, '')
|
||||
.split(',')
|
||||
.map((email) => ({ user_email: email }));
|
||||
const { project, services, priority, subject, description } = formValues;
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await fetch(
|
||||
`${process.env.NEXT_PUBLIC_ZENDESK_URL}/api/v2/requests.json`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${auth}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
request: {
|
||||
subject,
|
||||
comment: {
|
||||
body: description,
|
||||
},
|
||||
priority,
|
||||
requester: {
|
||||
name: user?.displayName,
|
||||
email: user?.email,
|
||||
},
|
||||
email_ccs: emails,
|
||||
custom_fields: [
|
||||
// these custom field IDs come from zendesk
|
||||
{
|
||||
id: 19502784542098,
|
||||
value: project,
|
||||
},
|
||||
{
|
||||
id: 19922709880978,
|
||||
value: services.map((service) =>
|
||||
service.value?.toLowerCase(),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
const response = await fetch('/api/support/create-ticket', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
);
|
||||
body: JSON.stringify({
|
||||
project,
|
||||
services,
|
||||
priority,
|
||||
subject,
|
||||
description,
|
||||
userName: user?.displayName,
|
||||
userEmail: user?.email,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to create ticket');
|
||||
}
|
||||
|
||||
form.reset();
|
||||
},
|
||||
{
|
||||
@@ -191,7 +167,7 @@ function TicketPage() {
|
||||
>
|
||||
{organizations.map((organization) => (
|
||||
<Option
|
||||
key={organization.name}
|
||||
key={organization.id}
|
||||
value={organization.id}
|
||||
label={organization.name}
|
||||
>
|
||||
@@ -261,6 +237,8 @@ function TicketPage() {
|
||||
slotProps={{
|
||||
root: { className: 'grid grid-flow-col gap-1 mb-4' },
|
||||
}}
|
||||
error={!!errors.priority}
|
||||
helperText={errors.priority?.message}
|
||||
renderValue={(option) => (
|
||||
<span className="inline-grid grid-flow-col items-center gap-2">
|
||||
{option?.label}
|
||||
@@ -310,7 +288,6 @@ function TicketPage() {
|
||||
label="Subject"
|
||||
placeholder="Summary of the problem you are experiencing"
|
||||
fullWidth
|
||||
autoFocus
|
||||
inputProps={{ min: 2, max: 128 }}
|
||||
error={!!errors.subject}
|
||||
helperText={errors.subject?.message}
|
||||
@@ -330,31 +307,16 @@ function TicketPage() {
|
||||
helperText={errors.description?.message}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Text className="mt-4 font-bold">Notifications</Text>
|
||||
|
||||
<StyledInput
|
||||
{...register('ccs')}
|
||||
id="ccs"
|
||||
label="CCs"
|
||||
placeholder="Comma separated list of emails you want to share this ticket with."
|
||||
fullWidth
|
||||
inputProps={{ min: 2, max: 128 }}
|
||||
error={!!errors.ccs}
|
||||
helperText={errors.ccs?.message}
|
||||
/>
|
||||
|
||||
<Box className="ml-auto flex w-80 flex-col gap-4">
|
||||
<Box className="ml-auto flex flex-col gap-4 lg:w-80">
|
||||
<Text color="secondary" className="text-right text-sm">
|
||||
We will contact you at <strong>{user?.email}</strong>
|
||||
</Text>
|
||||
<Button
|
||||
variant="outlined"
|
||||
className="hover:!bg-white hover:!bg-opacity-10 focus:ring-0"
|
||||
className="text-base hover:!bg-white hover:!bg-opacity-10 focus:ring-0"
|
||||
size="large"
|
||||
type="submit"
|
||||
startIcon={<EnvelopeIcon />}
|
||||
startIcon={<Mail className="size-4" />}
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
@@ -373,7 +335,7 @@ function TicketPage() {
|
||||
|
||||
TicketPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AuthenticatedLayout title="Help & Support | Nhost">
|
||||
<AuthenticatedLayout title="Help & Support | Nhost" withMainNav={false}>
|
||||
{page}
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import { mockOrganization, mockOrganizations } from '@/tests/mocks';
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export const getOrganizations = nhostGraphQLLink.query(
|
||||
'getOrganizations',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
organizations: mockOrganizations,
|
||||
}),
|
||||
),
|
||||
export const getOrganizations = nhostGraphQLLink.query('getOrganizations', () =>
|
||||
HttpResponse.json({
|
||||
data: { organizations: mockOrganizations },
|
||||
}),
|
||||
);
|
||||
|
||||
export const getOrganization = nhostGraphQLLink.query(
|
||||
'getOrganization',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
organizations: [{ ...mockOrganization }],
|
||||
}),
|
||||
),
|
||||
export const getOrganization = nhostGraphQLLink.query('getOrganization', () =>
|
||||
HttpResponse.json({
|
||||
data: { organizations: [{ ...mockOrganization }] },
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export const getPostgresSettings = nhostGraphQLLink.query(
|
||||
'GetPostgresSettings',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
systemConfig: {
|
||||
postgres: {
|
||||
database: 'gnlivtcgjxctuujxpslj',
|
||||
@@ -29,15 +30,15 @@ export const getPostgresSettings = nhostGraphQLLink.query(
|
||||
__typename: 'ConfigPostgres',
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const getPiTRNotEnabledPostgresSettings = nhostGraphQLLink.query(
|
||||
'GetPostgresSettings',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
systemConfig: {
|
||||
postgres: {
|
||||
database: 'gnlivtcgjxctuujxpslj',
|
||||
@@ -62,8 +63,6 @@ export const getPiTRNotEnabledPostgresSettings = nhostGraphQLLink.query(
|
||||
__typename: 'ConfigPostgres',
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// {"data":}
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
import { mockApplication } from '@/tests/mocks';
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export const getProjectQuery = nhostGraphQLLink.query(
|
||||
'getProject',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
apps: [{ ...mockApplication, githubRepository: null }],
|
||||
}),
|
||||
),
|
||||
export const getProjectQuery = nhostGraphQLLink.query('getProject', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication, githubRepository: null }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const getProjectStateQuery = (appStates?: any) =>
|
||||
nhostGraphQLLink.query('getProjectState', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
apps: [
|
||||
{
|
||||
...mockApplication,
|
||||
appStates: appStates || mockApplication.appStates,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const getNotFoundProjectStateQuery = nhostGraphQLLink.query(
|
||||
'getProjectState',
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
apps: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,144 +1,141 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export const getProjectsQuery = nhostGraphQLLink.query(
|
||||
'getProjects',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
apps: [
|
||||
{
|
||||
id: 'pitr-usa-id',
|
||||
name: 'pitr-not-enabled-usa',
|
||||
slug: 'pitr-not-enabled-usa',
|
||||
createdAt: '2025-03-10T12:35:23.193578+00:00',
|
||||
subdomain: 'ocrnpctsphttfxkuefyx',
|
||||
region: {
|
||||
id: '1',
|
||||
name: 'us-east-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-r-elek-id',
|
||||
email: 'robert@elek.com',
|
||||
displayName: 'Robert',
|
||||
__typename: 'users',
|
||||
},
|
||||
appStates: [
|
||||
{
|
||||
id: 'cd2b77ac-3ef1-4a76-819b-ff1caca09213',
|
||||
appId: 'pitr-usa-id',
|
||||
message:
|
||||
'failed to get dns manager: unknown region: 55985cd4-af14-4d2a-90a5-2a1253ebc1db',
|
||||
stateId: 8,
|
||||
createdAt: '2025-03-10T12:39:23.734345+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
export const getProjectsQuery = nhostGraphQLLink.query('getProjects', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
apps: [
|
||||
{
|
||||
id: 'pitr-usa-id',
|
||||
name: 'pitr-not-enabled-usa',
|
||||
slug: 'pitr-not-enabled-usa',
|
||||
createdAt: '2025-03-10T12:35:23.193578+00:00',
|
||||
subdomain: 'ocrnpctsphttfxkuefyx',
|
||||
region: {
|
||||
id: '1',
|
||||
name: 'us-east-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
{
|
||||
id: 'pitr-region-TEST-eu-id',
|
||||
name: 'pitr-region-test-eu',
|
||||
slug: 'pitr-region-test-eu',
|
||||
createdAt: '2025-03-10T12:45:40.813234+00:00',
|
||||
subdomain: 'doszbxwibtopsbfgbjpg',
|
||||
region: {
|
||||
id: 'dd6f8e01-35a9-4ba6-8dc6-ed972f2db93c',
|
||||
name: 'eu-central-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-r-elek-id',
|
||||
email: 'robert@elek.com',
|
||||
displayName: 'Robert',
|
||||
__typename: 'users',
|
||||
},
|
||||
appStates: [
|
||||
{
|
||||
id: 'c7fbf7ad-b60c-432b-86c2-5a9509054c47',
|
||||
appId: 'pitr-region-TEST-eu-id',
|
||||
message: '',
|
||||
stateId: 5,
|
||||
createdAt: '2025-03-12T11:08:59.926611+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-r-elek-id',
|
||||
email: 'robert@elek.com',
|
||||
displayName: 'Robert',
|
||||
__typename: 'users',
|
||||
},
|
||||
{
|
||||
id: 'pitr-test-id',
|
||||
name: 'pitr-test',
|
||||
slug: 'pitr-test',
|
||||
createdAt: '2025-03-04T13:48:59.76498+00:00',
|
||||
subdomain: 'gnlivtcgjxctuujxpslj',
|
||||
region: {
|
||||
id: '1',
|
||||
name: 'us-east-1',
|
||||
__typename: 'regions',
|
||||
appStates: [
|
||||
{
|
||||
id: 'cd2b77ac-3ef1-4a76-819b-ff1caca09213',
|
||||
appId: 'pitr-usa-id',
|
||||
message:
|
||||
'failed to get dns manager: unknown region: 55985cd4-af14-4d2a-90a5-2a1253ebc1db',
|
||||
stateId: 8,
|
||||
createdAt: '2025-03-10T12:39:23.734345+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
},
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-d-elek-id',
|
||||
email: 'dbarrosop@dravetech.com',
|
||||
displayName: 'David Elek',
|
||||
__typename: 'users',
|
||||
},
|
||||
appStates: [
|
||||
{
|
||||
id: 'fc344bc6-1c59-447a-813f-e0f65754b0e0',
|
||||
appId: 'pitr-test-id',
|
||||
message:
|
||||
'failed to deploy application to kubernetes: failed to deploy application: failed to check rollout status: error running kubectl: exit status 1',
|
||||
stateId: 8,
|
||||
createdAt: '2025-03-11T15:34:41.25304+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
],
|
||||
__typename: 'apps',
|
||||
},
|
||||
{
|
||||
id: 'pitr-region-TEST-eu-id',
|
||||
name: 'pitr-region-test-eu',
|
||||
slug: 'pitr-region-test-eu',
|
||||
createdAt: '2025-03-10T12:45:40.813234+00:00',
|
||||
subdomain: 'doszbxwibtopsbfgbjpg',
|
||||
region: {
|
||||
id: 'dd6f8e01-35a9-4ba6-8dc6-ed972f2db93c',
|
||||
name: 'eu-central-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
{
|
||||
id: 'pitr14-id',
|
||||
name: 'pitr14',
|
||||
slug: 'pitr14',
|
||||
createdAt: '2025-02-25T08:55:22.82937+00:00',
|
||||
subdomain: 'jqumebxpocjytrhevonb',
|
||||
region: {
|
||||
id: '1',
|
||||
name: 'us-east-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-d-elek-id',
|
||||
email: 'david@elek.com',
|
||||
displayName: 'David Elek',
|
||||
__typename: 'users',
|
||||
},
|
||||
appStates: [
|
||||
{
|
||||
id: '04bc2db3-a948-48fb-b674-7a8a0133dd2b',
|
||||
appId: 'pitr14-id',
|
||||
message: '',
|
||||
stateId: 5,
|
||||
createdAt: '2025-03-11T20:47:03.102948+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-r-elek-id',
|
||||
email: 'robert@elek.com',
|
||||
displayName: 'Robert',
|
||||
__typename: 'users',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
appStates: [
|
||||
{
|
||||
id: 'c7fbf7ad-b60c-432b-86c2-5a9509054c47',
|
||||
appId: 'pitr-region-TEST-eu-id',
|
||||
message: '',
|
||||
stateId: 5,
|
||||
createdAt: '2025-03-12T11:08:59.926611+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
},
|
||||
{
|
||||
id: 'pitr-test-id',
|
||||
name: 'pitr-test',
|
||||
slug: 'pitr-test',
|
||||
createdAt: '2025-03-04T13:48:59.76498+00:00',
|
||||
subdomain: 'gnlivtcgjxctuujxpslj',
|
||||
region: {
|
||||
id: '1',
|
||||
name: 'us-east-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-d-elek-id',
|
||||
email: 'dbarrosop@dravetech.com',
|
||||
displayName: 'David Elek',
|
||||
__typename: 'users',
|
||||
},
|
||||
appStates: [
|
||||
{
|
||||
id: 'fc344bc6-1c59-447a-813f-e0f65754b0e0',
|
||||
appId: 'pitr-test-id',
|
||||
message:
|
||||
'failed to deploy application to kubernetes: failed to deploy application: failed to check rollout status: error running kubectl: exit status 1',
|
||||
stateId: 8,
|
||||
createdAt: '2025-03-11T15:34:41.25304+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
},
|
||||
{
|
||||
id: 'pitr14-id',
|
||||
name: 'pitr14',
|
||||
slug: 'pitr14',
|
||||
createdAt: '2025-02-25T08:55:22.82937+00:00',
|
||||
subdomain: 'jqumebxpocjytrhevonb',
|
||||
region: {
|
||||
id: '1',
|
||||
name: 'us-east-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-d-elek-id',
|
||||
email: 'david@elek.com',
|
||||
displayName: 'David Elek',
|
||||
__typename: 'users',
|
||||
},
|
||||
appStates: [
|
||||
{
|
||||
id: '04bc2db3-a948-48fb-b674-7a8a0133dd2b',
|
||||
appId: 'pitr14-id',
|
||||
message: '',
|
||||
stateId: 5,
|
||||
createdAt: '2025-03-11T20:47:03.102948+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const getEmptyProjectsQuery = nhostGraphQLLink.query(
|
||||
'getProjects',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
apps: [],
|
||||
}),
|
||||
),
|
||||
export const getEmptyProjectsQuery = nhostGraphQLLink.query('getProjects', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
apps: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export const organizationMemberInvites = nhostGraphQLLink.query(
|
||||
'organizationMemberInvites',
|
||||
(_req, res, ctx) => res(ctx.data({ organizationMemberInvites: [] })),
|
||||
() => HttpResponse.json({ data: { organizationMemberInvites: [] } }),
|
||||
);
|
||||
|
||||
export const organizationNewRequests = nhostGraphQLLink.query(
|
||||
'organizationNewRequests',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
organizationNewRequests: [
|
||||
{
|
||||
id: 'org-request-id-1',
|
||||
@@ -17,6 +18,6 @@ export const organizationNewRequests = nhostGraphQLLink.query(
|
||||
__typename: 'organization_new_request',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
const permissionVariablesQuery = nhostGraphQLLink.query(
|
||||
'GetRolesPermissions',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.delay(250),
|
||||
ctx.data({
|
||||
async () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
config: {
|
||||
auth: {
|
||||
user: {
|
||||
@@ -32,8 +32,8 @@ const permissionVariablesQuery = nhostGraphQLLink.query(
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export default permissionVariablesQuery;
|
||||
|
||||
@@ -1,50 +1,46 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
/**
|
||||
* Use this handler to simulate a query that returns only the Pro plan.
|
||||
*/
|
||||
export const getProPlanOnlyQuery = nhostGraphQLLink.query(
|
||||
'GetPlans',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
plans: [
|
||||
{
|
||||
__typename: 'plans',
|
||||
id: 'dc5e805e-1bef-4d43-809e-9fdf865e211a',
|
||||
name: 'Pro',
|
||||
price: 25,
|
||||
isFree: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
export const getProPlanOnlyQuery = nhostGraphQLLink.query('GetPlans', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
plans: [
|
||||
{
|
||||
__typename: 'plans',
|
||||
id: 'dc5e805e-1bef-4d43-809e-9fdf865e211a',
|
||||
name: 'Pro',
|
||||
price: 25,
|
||||
isFree: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Use this handler to simulate a query that returns all the available plans.
|
||||
*/
|
||||
export const getAllPlansQuery = nhostGraphQLLink.query(
|
||||
'GetPlans',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
plans: [
|
||||
{
|
||||
__typename: 'plans',
|
||||
id: '00000000-0000-0000-0000-000000000000',
|
||||
name: 'Starter',
|
||||
price: 0,
|
||||
isFree: true,
|
||||
},
|
||||
{
|
||||
__typename: 'plans',
|
||||
id: '00000000-0000-0000-0000-000000000001',
|
||||
name: 'Pro',
|
||||
price: 25,
|
||||
isFree: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
export const getAllPlansQuery = nhostGraphQLLink.query('GetPlans', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
plans: [
|
||||
{
|
||||
__typename: 'plans',
|
||||
id: '00000000-0000-0000-0000-000000000000',
|
||||
name: 'Starter',
|
||||
price: 0,
|
||||
isFree: true,
|
||||
},
|
||||
{
|
||||
__typename: 'plans',
|
||||
id: '00000000-0000-0000-0000-000000000001',
|
||||
name: 'Pro',
|
||||
price: 25,
|
||||
isFree: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export const prefetchNewAppQuery = nhostGraphQLLink.query(
|
||||
'PrefetchNewApp',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
regions: [
|
||||
{
|
||||
id: 'dd6f8e01-35a9-4ba6-8dc6-ed972f2db93c',
|
||||
@@ -67,6 +68,6 @@ export const prefetchNewAppQuery = nhostGraphQLLink.query(
|
||||
__typename: 'plans',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
/**
|
||||
* Use this handler to simulate the initial state of the allocated resources.
|
||||
*/
|
||||
export const resourcesUnavailableQuery = nhostGraphQLLink.query(
|
||||
'GetResources',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
config: {
|
||||
__typename: 'ConfigConfig',
|
||||
postgres: {
|
||||
@@ -23,8 +23,8 @@ export const resourcesUnavailableQuery = nhostGraphQLLink.query(
|
||||
resources: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -32,9 +32,9 @@ export const resourcesUnavailableQuery = nhostGraphQLLink.query(
|
||||
*/
|
||||
export const resourcesAvailableQuery = nhostGraphQLLink.query(
|
||||
'GetResources',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
config: {
|
||||
__typename: 'ConfigConfig',
|
||||
postgres: {
|
||||
@@ -86,8 +86,8 @@ export const resourcesAvailableQuery = nhostGraphQLLink.query(
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -95,9 +95,9 @@ export const resourcesAvailableQuery = nhostGraphQLLink.query(
|
||||
*/
|
||||
export const resourcesUpdatedQuery = nhostGraphQLLink.query(
|
||||
'GetResources',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
config: {
|
||||
__typename: 'ConfigConfig',
|
||||
postgres: {
|
||||
@@ -137,6 +137,6 @@ export const resourcesUpdatedQuery = nhostGraphQLLink.query(
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export default nhostGraphQLLink.mutation('UpdateConfig', (req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
export default nhostGraphQLLink.mutation('UpdateConfig', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
updateConfig: {
|
||||
id: 'ConfigConfig',
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,419 +1,415 @@
|
||||
import { rest } from 'msw';
|
||||
import { delay, http, HttpResponse } from 'msw';
|
||||
|
||||
const hasuraMetadataQuery = rest.post(
|
||||
const hasuraMetadataQuery = http.post(
|
||||
'https://local.hasura.local.nhost.run/v1/metadata',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.delay(250),
|
||||
ctx.json({
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{
|
||||
table: { name: 'authors', schema: 'public' },
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'books',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'author_id',
|
||||
table: { name: 'books', schema: 'public' },
|
||||
},
|
||||
async () => {
|
||||
await delay(250);
|
||||
|
||||
return HttpResponse.json({
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{
|
||||
table: { name: 'authors', schema: 'public' },
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'books',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'author_id',
|
||||
table: { name: 'books', schema: 'public' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: { name: 'books', schema: 'public' },
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'author',
|
||||
using: { foreign_key_constraint_on: 'author_id' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: { from_env: 'HASURA_GRAPHQL_DATABASE_URL' },
|
||||
isolation_level: 'read-committed',
|
||||
pool_settings: {
|
||||
connection_lifetime: 600,
|
||||
idle_timeout: 180,
|
||||
max_connections: 50,
|
||||
retries: 1,
|
||||
},
|
||||
use_prepared_statements: true,
|
||||
],
|
||||
},
|
||||
{
|
||||
table: { name: 'books', schema: 'public' },
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'author',
|
||||
using: { foreign_key_constraint_on: 'author_id' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: { from_env: 'HASURA_GRAPHQL_DATABASE_URL' },
|
||||
isolation_level: 'read-committed',
|
||||
pool_settings: {
|
||||
connection_lifetime: 600,
|
||||
idle_timeout: 180,
|
||||
max_connections: 50,
|
||||
retries: 1,
|
||||
},
|
||||
use_prepared_statements: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
resource_version: 10,
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
resource_version: 10,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const hasuraRelationShipsMetadataQuery = rest.post(
|
||||
export const hasuraRelationShipsMetadataQuery = http.post(
|
||||
'https://local.hasura.local.nhost.run/v1/metadata',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
resource_version: 26,
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{
|
||||
table: {
|
||||
name: 'country',
|
||||
schema: 'public',
|
||||
},
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'county',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'countryId',
|
||||
table: {
|
||||
name: 'county',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
resource_version: 26,
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{
|
||||
table: {
|
||||
name: 'country',
|
||||
schema: 'public',
|
||||
},
|
||||
{
|
||||
table: {
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'county',
|
||||
schema: 'public',
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'country',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'countryId',
|
||||
},
|
||||
},
|
||||
],
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'town',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'countyId',
|
||||
table: {
|
||||
name: 'town',
|
||||
schema: 'public',
|
||||
},
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'countryId',
|
||||
table: {
|
||||
name: 'county',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'town',
|
||||
schema: 'public',
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'county',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'countyId',
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'county',
|
||||
schema: 'public',
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'country',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'countryId',
|
||||
},
|
||||
},
|
||||
],
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'town',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'countyId',
|
||||
table: {
|
||||
name: 'town',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: {
|
||||
from_env: 'HASURA_GRAPHQL_DATABASE_URL',
|
||||
},
|
||||
isolation_level: 'read-committed',
|
||||
pool_settings: {
|
||||
connection_lifetime: 600,
|
||||
idle_timeout: 180,
|
||||
max_connections: 50,
|
||||
retries: 1,
|
||||
},
|
||||
use_prepared_statements: true,
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'town',
|
||||
schema: 'public',
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'county',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'countyId',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: {
|
||||
from_env: 'HASURA_GRAPHQL_DATABASE_URL',
|
||||
},
|
||||
isolation_level: 'read-committed',
|
||||
pool_settings: {
|
||||
connection_lifetime: 600,
|
||||
idle_timeout: 180,
|
||||
max_connections: 50,
|
||||
retries: 1,
|
||||
},
|
||||
use_prepared_statements: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const hasuraColumnMetadataQuery = rest.post(
|
||||
export const hasuraColumnMetadataQuery = http.post(
|
||||
'https://local.hasura.local.nhost.run/v1/metadata',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
resource_version: 389,
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{
|
||||
table: {
|
||||
name: 'actor',
|
||||
schema: 'public',
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'actor_movie',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'actor_id',
|
||||
table: {
|
||||
name: 'actor_movie',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
resource_version: 389,
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{
|
||||
table: {
|
||||
name: 'actor',
|
||||
schema: 'public',
|
||||
},
|
||||
{
|
||||
table: {
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'actor_movie',
|
||||
schema: 'public',
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'actor',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'actor_id',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'movie',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'movie_id',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'director',
|
||||
schema: 'public',
|
||||
},
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'movies',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'director_id',
|
||||
table: {
|
||||
name: 'movies',
|
||||
schema: 'public',
|
||||
},
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'actor_id',
|
||||
table: {
|
||||
name: 'actor_movie',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'actor_movie',
|
||||
schema: 'public',
|
||||
},
|
||||
{
|
||||
table: {
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'actor',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'actor_id',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'movie',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'movie_id',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'director',
|
||||
schema: 'public',
|
||||
},
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'movies',
|
||||
schema: 'public',
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'author',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'director_id',
|
||||
},
|
||||
},
|
||||
],
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'actor_movie',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'movie_id',
|
||||
table: {
|
||||
name: 'actor_movie',
|
||||
schema: 'public',
|
||||
},
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'director_id',
|
||||
table: {
|
||||
name: 'movies',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'movies',
|
||||
schema: 'public',
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'notes',
|
||||
schema: 'public',
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'author',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'director_id',
|
||||
},
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'user',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'owner',
|
||||
},
|
||||
},
|
||||
],
|
||||
insert_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
check: {
|
||||
owner: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
columns: ['id', 'note', 'owner'],
|
||||
},
|
||||
},
|
||||
],
|
||||
select_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: ['id', 'note'],
|
||||
filter: {
|
||||
owner: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
],
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'actor_movie',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'movie_id',
|
||||
table: {
|
||||
name: 'actor_movie',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
update_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: ['note'],
|
||||
filter: {
|
||||
owner: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
check: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
delete_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
filter: {
|
||||
id: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'notes',
|
||||
schema: 'public',
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'buckets',
|
||||
schema: 'storage',
|
||||
},
|
||||
configuration: {
|
||||
column_config: {
|
||||
cache_control: {
|
||||
custom_name: 'cacheControl',
|
||||
},
|
||||
created_at: {
|
||||
custom_name: 'createdAt',
|
||||
},
|
||||
download_expiration: {
|
||||
custom_name: 'downloadExpiration',
|
||||
},
|
||||
id: {
|
||||
custom_name: 'id',
|
||||
},
|
||||
max_upload_file_size: {
|
||||
custom_name: 'maxUploadFileSize',
|
||||
},
|
||||
min_upload_file_size: {
|
||||
custom_name: 'minUploadFileSize',
|
||||
},
|
||||
presigned_urls_enabled: {
|
||||
custom_name: 'presignedUrlsEnabled',
|
||||
},
|
||||
updated_at: {
|
||||
custom_name: 'updatedAt',
|
||||
},
|
||||
},
|
||||
custom_column_names: {
|
||||
cache_control: 'cacheControl',
|
||||
created_at: 'createdAt',
|
||||
download_expiration: 'downloadExpiration',
|
||||
id: 'id',
|
||||
max_upload_file_size: 'maxUploadFileSize',
|
||||
min_upload_file_size: 'minUploadFileSize',
|
||||
presigned_urls_enabled: 'presignedUrlsEnabled',
|
||||
updated_at: 'updatedAt',
|
||||
},
|
||||
custom_name: 'buckets',
|
||||
custom_root_fields: {
|
||||
delete: 'deleteBuckets',
|
||||
delete_by_pk: 'deleteBucket',
|
||||
insert: 'insertBuckets',
|
||||
insert_one: 'insertBucket',
|
||||
select: 'buckets',
|
||||
select_aggregate: 'bucketsAggregate',
|
||||
select_by_pk: 'bucket',
|
||||
update: 'updateBuckets',
|
||||
update_by_pk: 'updateBucket',
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'user',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'owner',
|
||||
},
|
||||
},
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'files',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'bucket_id',
|
||||
table: {
|
||||
name: 'files',
|
||||
schema: 'storage',
|
||||
},
|
||||
],
|
||||
insert_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
check: {
|
||||
owner: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
columns: ['id', 'note', 'owner'],
|
||||
},
|
||||
},
|
||||
],
|
||||
select_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: ['id', 'note'],
|
||||
filter: {
|
||||
owner: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: {
|
||||
from_env: 'HASURA_GRAPHQL_DATABASE_URL',
|
||||
},
|
||||
isolation_level: 'read-committed',
|
||||
pool_settings: {
|
||||
connection_lifetime: 600,
|
||||
idle_timeout: 180,
|
||||
max_connections: 50,
|
||||
retries: 1,
|
||||
],
|
||||
update_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: ['note'],
|
||||
filter: {
|
||||
owner: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
check: null,
|
||||
},
|
||||
},
|
||||
use_prepared_statements: true,
|
||||
],
|
||||
delete_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
filter: {
|
||||
id: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'buckets',
|
||||
schema: 'storage',
|
||||
},
|
||||
configuration: {
|
||||
column_config: {
|
||||
cache_control: {
|
||||
custom_name: 'cacheControl',
|
||||
},
|
||||
created_at: {
|
||||
custom_name: 'createdAt',
|
||||
},
|
||||
download_expiration: {
|
||||
custom_name: 'downloadExpiration',
|
||||
},
|
||||
id: {
|
||||
custom_name: 'id',
|
||||
},
|
||||
max_upload_file_size: {
|
||||
custom_name: 'maxUploadFileSize',
|
||||
},
|
||||
min_upload_file_size: {
|
||||
custom_name: 'minUploadFileSize',
|
||||
},
|
||||
presigned_urls_enabled: {
|
||||
custom_name: 'presignedUrlsEnabled',
|
||||
},
|
||||
updated_at: {
|
||||
custom_name: 'updatedAt',
|
||||
},
|
||||
},
|
||||
custom_column_names: {
|
||||
cache_control: 'cacheControl',
|
||||
created_at: 'createdAt',
|
||||
download_expiration: 'downloadExpiration',
|
||||
id: 'id',
|
||||
max_upload_file_size: 'maxUploadFileSize',
|
||||
min_upload_file_size: 'minUploadFileSize',
|
||||
presigned_urls_enabled: 'presignedUrlsEnabled',
|
||||
updated_at: 'updatedAt',
|
||||
},
|
||||
custom_name: 'buckets',
|
||||
custom_root_fields: {
|
||||
delete: 'deleteBuckets',
|
||||
delete_by_pk: 'deleteBucket',
|
||||
insert: 'insertBuckets',
|
||||
insert_one: 'insertBucket',
|
||||
select: 'buckets',
|
||||
select_aggregate: 'bucketsAggregate',
|
||||
select_by_pk: 'bucket',
|
||||
update: 'updateBuckets',
|
||||
update_by_pk: 'updateBucket',
|
||||
},
|
||||
},
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'files',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'bucket_id',
|
||||
table: {
|
||||
name: 'files',
|
||||
schema: 'storage',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: {
|
||||
from_env: 'HASURA_GRAPHQL_DATABASE_URL',
|
||||
},
|
||||
isolation_level: 'read-committed',
|
||||
pool_settings: {
|
||||
connection_lifetime: 600,
|
||||
idle_timeout: 180,
|
||||
max_connections: 50,
|
||||
retries: 1,
|
||||
},
|
||||
use_prepared_statements: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export default hasuraMetadataQuery;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user