Compare commits
131 Commits
@nhost/nho
...
@nhost/nho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc88dbc4bd | ||
|
|
a6a5ecdad9 | ||
|
|
069896c2d9 | ||
|
|
754541a24b | ||
|
|
e7978b0346 | ||
|
|
894cd29d6b | ||
|
|
d570084d24 | ||
|
|
8c71dd9db9 | ||
|
|
c6006fec30 | ||
|
|
69c2954658 | ||
|
|
be6af4f157 | ||
|
|
3a41251caf | ||
|
|
c6af08fde4 | ||
|
|
63d73e639c | ||
|
|
8112625a0a | ||
|
|
bbf1f6c11d | ||
|
|
d37f31fc41 | ||
|
|
0367dfae00 | ||
|
|
6ad1cfcb13 | ||
|
|
25c0ffa83b | ||
|
|
cc98f33440 | ||
|
|
8812d9dcaf | ||
|
|
bf17981596 | ||
|
|
2f4b3768c7 | ||
|
|
73a7ba82ae | ||
|
|
ba3c49e443 | ||
|
|
88836f3b1f | ||
|
|
81716d9d9c | ||
|
|
a30da08e9b | ||
|
|
397bfc948c | ||
|
|
0d183761ae | ||
|
|
1902a114ec | ||
|
|
92e71a61f9 | ||
|
|
9790bcfe3e | ||
|
|
811b48eccf | ||
|
|
57987ed3a9 | ||
|
|
7f0db210ba | ||
|
|
d8c5117046 | ||
|
|
7633d04121 | ||
|
|
e8a378906a | ||
|
|
34ede5cf2c | ||
|
|
2deeb39a28 | ||
|
|
d98e73e57e | ||
|
|
4c6400fc52 | ||
|
|
c4f383f695 | ||
|
|
1708578f8f | ||
|
|
96228dfe69 | ||
|
|
2f5bc04e0c | ||
|
|
06b47e0fb9 | ||
|
|
412692c2f6 | ||
|
|
89f6fe6346 | ||
|
|
2e34d7b9d0 | ||
|
|
66e0cc8261 | ||
|
|
7eb9539807 | ||
|
|
906620a755 | ||
|
|
5e9ddb41d2 | ||
|
|
00132bd961 | ||
|
|
57b26152e4 | ||
|
|
5565451f18 | ||
|
|
4b18e02ad2 | ||
|
|
181c0ab19d | ||
|
|
939a158917 | ||
|
|
9c0a118721 | ||
|
|
129ec1edfc | ||
|
|
40439b9987 | ||
|
|
cffa161da7 | ||
|
|
4ffff86752 | ||
|
|
f9e170e958 | ||
|
|
b8cb491ab1 | ||
|
|
59249e5161 | ||
|
|
df6b85e98c | ||
|
|
85316e822f | ||
|
|
f7d7080dad | ||
|
|
ec24567d83 | ||
|
|
56c87dad64 | ||
|
|
47ab341ce4 | ||
|
|
5fed49e05b | ||
|
|
aee9a80ac8 | ||
|
|
5ef3f76ea0 | ||
|
|
4ca9641304 | ||
|
|
fd3b5c77e4 | ||
|
|
9ed8ce8a5e | ||
|
|
e7762cb2b5 | ||
|
|
e353d99de8 | ||
|
|
c4d289a4d5 | ||
|
|
e2065e22df | ||
|
|
d738884d7d | ||
|
|
b50404566f | ||
|
|
8caf3daa54 | ||
|
|
8a07613cbe | ||
|
|
736862c9cc | ||
|
|
ea99fb31d7 | ||
|
|
70433187cc | ||
|
|
39b10a2e9f | ||
|
|
4b8478004e | ||
|
|
61eb6cdc2d | ||
|
|
14187d381f | ||
|
|
99b78f147e | ||
|
|
2aa81a6cb9 | ||
|
|
a1edaf18ea | ||
|
|
4d835c4b9c | ||
|
|
44a3e6bd41 | ||
|
|
6ee2d1f5bf | ||
|
|
df51c3e64e | ||
|
|
9acae7d1c4 | ||
|
|
f6947a2194 | ||
|
|
31e636a9c8 | ||
|
|
0fdff345ac | ||
|
|
97db63791b | ||
|
|
a0931e282f | ||
|
|
e87505c564 | ||
|
|
c0635ae1c7 | ||
|
|
d2a9a9ae1d | ||
|
|
c97b43f149 | ||
|
|
2026bb7a9c | ||
|
|
1bc1e30f5e | ||
|
|
85526782f2 | ||
|
|
fad7f640de | ||
|
|
5ff4dd6e40 | ||
|
|
0bf28085b7 | ||
|
|
b302dbd27d | ||
|
|
72a365c5fc | ||
|
|
d11363a74c | ||
|
|
1bc2fabe59 | ||
|
|
f8243f9434 | ||
|
|
d9eb90604d | ||
|
|
cef647194d | ||
|
|
efd68c3f92 | ||
|
|
233232b06f | ||
|
|
5e962300f6 | ||
|
|
048b3389e6 |
@@ -1,9 +0,0 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool
|
||||
that works with multi-package repos, or single-package repos to help you version and publish your
|
||||
code. You can find the full documentation for it
|
||||
[in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch"
|
||||
}
|
||||
14
.github/CODEOWNERS
vendored
14
.github/CODEOWNERS
vendored
@@ -1,14 +0,0 @@
|
||||
# Documentation
|
||||
# https://help.github.com/en/articles/about-code-owners
|
||||
|
||||
/packages @nunopato @onehassan
|
||||
/packages/docgen @nunopato @onehassan
|
||||
/integrations/stripe-graphql-js @nunopato @onehassan
|
||||
/.github @nunopato @onehassan
|
||||
/dashboard/ @nunopato @onehassan
|
||||
/docs/ @nunopato @onehassan
|
||||
/config/ @nunopato @onehassan
|
||||
/examples/ @nunopato @onehassan
|
||||
/examples/codegen-react-apollo @nunopato @onehassan
|
||||
/examples/codegen-react-query @nunopato @onehassan
|
||||
/examples/react-apollo-crm @nunopato @onehassan
|
||||
39
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
39
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
### Checklist
|
||||
|
||||
- [ ] No breaking changes
|
||||
- [ ] Tests pass
|
||||
- [ ] New features have new tests
|
||||
- [ ] Documentation is updated (if applicable)
|
||||
- [ ] Title of the PR is in the correct format (see below)
|
||||
|
||||
--- Delete everything below this line before submitting your PR ---
|
||||
|
||||
### PR title format
|
||||
|
||||
The PR title must follow the following pattern:
|
||||
|
||||
`TYPE(PKG): SUMMARY`
|
||||
|
||||
Where `TYPE` is:
|
||||
|
||||
- feat: mark this pull request as a feature
|
||||
- fix: mark this pull request as a bug fix
|
||||
- chore: mark this pull request as a maintenance item
|
||||
|
||||
Where `PKG` is:
|
||||
|
||||
- `ci`: For general changes to the build and/or CI/CD pipeline
|
||||
- `codegen`: For changes to the code generator
|
||||
- `dashboard`: For changes to the Nhost Dashboard
|
||||
- `docs`: For changes to the documentation
|
||||
- `examples`: For changes to the examples
|
||||
- `mintlify-openapi`: For changes to the Mintlify OpenAPI tool
|
||||
- `nhost-js`: For changes to the Nhost JavaScript SDK
|
||||
- `nixops`: For changes to the NixOps
|
||||
|
||||
Where `SUMMARY` is a short description of what the PR does.
|
||||
|
||||
### Tests
|
||||
|
||||
- please make sure your changes pass the current tests (Use the `make test`
|
||||
- if you are introducing a new feature, please write as much tests as possible.
|
||||
29
.github/actions/discord-notification/action.yml
vendored
Normal file
29
.github/actions/discord-notification/action.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: 'Discord Notification'
|
||||
description: 'Send a Discord notification with conditional check'
|
||||
|
||||
inputs:
|
||||
webhook-url:
|
||||
description: 'Discord webhook URL'
|
||||
required: true
|
||||
title:
|
||||
description: 'Embed title'
|
||||
required: true
|
||||
description:
|
||||
description: 'Embed description'
|
||||
required: true
|
||||
color:
|
||||
description: 'Embed color (decimal number)'
|
||||
required: false
|
||||
default: '5763719'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Send Discord notification
|
||||
if: ${{ inputs.webhook-url }}
|
||||
uses: tsickert/discord-webhook@v7.0.0
|
||||
with:
|
||||
webhook-url: ${{ inputs.webhook-url }}
|
||||
embed-title: ${{ inputs.title }}
|
||||
embed-description: ${{ inputs.description }}
|
||||
embed-color: ${{ inputs.color }}
|
||||
59
.github/actions/install-dependencies/action.yaml
vendored
59
.github/actions/install-dependencies/action.yaml
vendored
@@ -1,59 +0,0 @@
|
||||
name: Install Node and package dependencies
|
||||
description: 'Install Node dependencies with pnpm'
|
||||
inputs:
|
||||
TURBO_TOKEN:
|
||||
description: 'Turborepo token'
|
||||
TURBO_TEAM:
|
||||
description: 'Turborepo team'
|
||||
BUILD:
|
||||
description: 'Build packages'
|
||||
default: 'default'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.1.0
|
||||
run_install: false
|
||||
- name: Get pnpm cache directory
|
||||
id: pnpm-cache-dir
|
||||
shell: bash
|
||||
run: echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v4
|
||||
id: pnpm-cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: ${{ runner.os }}-node-
|
||||
- name: Use Node.js v20
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
- shell: bash
|
||||
name: Use Latest Corepack
|
||||
run: |
|
||||
echo "Before: corepack version => $(corepack --version || echo 'not installed')"
|
||||
npm install -g corepack@latest
|
||||
echo "After : corepack version => $(corepack --version)"
|
||||
corepack enable
|
||||
pnpm --version
|
||||
- shell: bash
|
||||
name: Install packages
|
||||
run: pnpm install --frozen-lockfile
|
||||
# * Build all Nhost packages as they are all supposed to be tested.
|
||||
# * They are reused through the Turborepo cache
|
||||
- shell: bash
|
||||
name: Build packages
|
||||
if: ${{ inputs.BUILD == 'all' }}
|
||||
run: pnpm run build:all
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
- shell: bash
|
||||
name: Build everything in the monorepo
|
||||
if: ${{ inputs.BUILD == 'default' }}
|
||||
run: pnpm run build
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
108
.github/actions/nhost-cli/README.md
vendored
108
.github/actions/nhost-cli/README.md
vendored
@@ -1,108 +0,0 @@
|
||||
# Nhost CLI GitHub Action
|
||||
|
||||
## Usage
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
```
|
||||
|
||||
### Install the CLI and start the app
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI and start the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
start: true
|
||||
```
|
||||
|
||||
### Set another working directory
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: examples/react-apollo
|
||||
start: true
|
||||
```
|
||||
|
||||
### Don't wait for the app to be ready
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI and start app
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
start: true
|
||||
wait: false
|
||||
```
|
||||
|
||||
### Stop the app
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Start app
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
start: true
|
||||
- name: Do something
|
||||
cmd: echo "do something"
|
||||
- name: Stop
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
stop: true
|
||||
```
|
||||
|
||||
### Install a given value of the CLI
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
version: v0.8.10
|
||||
```
|
||||
|
||||
### Inject values into nhost/config.yaml
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
config: |
|
||||
services:
|
||||
auth:
|
||||
image: nhost/hasura-auth:0.16.1
|
||||
```
|
||||
84
.github/actions/nhost-cli/action.yaml
vendored
84
.github/actions/nhost-cli/action.yaml
vendored
@@ -1,84 +0,0 @@
|
||||
name: Nhost CLI
|
||||
description: 'Action to install the Nhost CLI and to run an application'
|
||||
inputs:
|
||||
init:
|
||||
description: 'Initialize the application'
|
||||
default: 'false'
|
||||
start:
|
||||
description: "Start the application. If false, the application won't be started"
|
||||
default: 'false'
|
||||
wait:
|
||||
description: 'If starting the application, wait until it is ready'
|
||||
default: 'true'
|
||||
stop:
|
||||
description: 'Stop the application'
|
||||
default: 'false'
|
||||
path:
|
||||
description: 'Path to the application'
|
||||
default: '.'
|
||||
version:
|
||||
description: 'Version of the Nhost CLI'
|
||||
default: 'latest'
|
||||
dashboard-image:
|
||||
description: 'Image of the dashboard'
|
||||
default: 'nhost/dashboard:latest'
|
||||
config:
|
||||
description: 'Values to be injected into nhost/config.yaml'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Check if Nhost CLI is already installed
|
||||
id: check-nhost-cli
|
||||
shell: bash
|
||||
# TODO check if the version is the same
|
||||
run: |
|
||||
if [ -z "$(which nhost)" ]
|
||||
then
|
||||
echo "installed=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "installed=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Install Nhost CLI
|
||||
if: ${{ steps.check-nhost-cli.outputs.installed == 'false' }}
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 3
|
||||
max_attempts: 10
|
||||
command: bash <(curl --silent -L https://raw.githubusercontent.com/nhost/cli/main/get.sh) ${{ inputs.version }}
|
||||
- name: Initialize a new project from scratch
|
||||
if: ${{ inputs.init == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: |
|
||||
rm -rf ./*
|
||||
nhost init
|
||||
- name: Set custom configuration
|
||||
if: ${{ inputs.config }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: config="${{ inputs.config }}" yq -i '. *= env(config)' nhost/config.yaml
|
||||
- name: Start the application
|
||||
if: ${{ inputs.start == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: |
|
||||
if [ -n "${{ inputs.dashboard-image }}" ]; then
|
||||
export NHOST_DASHBOARD_VERSION=${{ inputs.dashboard-image }}
|
||||
fi
|
||||
if [ -f .secrets.example ]; then
|
||||
cp .secrets.example .secrets
|
||||
fi
|
||||
nhost up
|
||||
- name: Log on failure
|
||||
if: steps.wait.outcome == 'failure'
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: |
|
||||
nhost logs
|
||||
exit 1
|
||||
- name: Stop the application
|
||||
if: ${{ inputs.stop == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: nhost down
|
||||
24
.github/labeler.yml
vendored
24
.github/labeler.yml
vendored
@@ -1,24 +0,0 @@
|
||||
dashboard:
|
||||
- dashboard/**/*
|
||||
|
||||
documentation:
|
||||
- any:
|
||||
- docs/**/*
|
||||
|
||||
examples:
|
||||
- examples/**/*
|
||||
|
||||
sdk:
|
||||
- packages/**/*
|
||||
|
||||
integrations:
|
||||
- integrations/**/*
|
||||
|
||||
react:
|
||||
- '{packages,examples,integrations}/*react*/**/*'
|
||||
|
||||
nextjs:
|
||||
- '{packages,examples}/*next*/**/*'
|
||||
|
||||
vue:
|
||||
- '{packages,examples,integrations}/*vue*/**/*'
|
||||
157
.github/workflows/changesets.yaml
vendored
157
.github/workflows/changesets.yaml
vendored
@@ -1,157 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'examples/**'
|
||||
- 'assets/**'
|
||||
- '**.md'
|
||||
- '!.changeset/**'
|
||||
- 'LICENSE'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: nhost
|
||||
DASHBOARD_PACKAGE: '@nhost/dashboard'
|
||||
|
||||
jobs:
|
||||
version:
|
||||
name: Version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
hasChangesets: ${{ steps.changesets.outputs.hasChangesets }}
|
||||
dashboardVersion: ${{ steps.dashboard.outputs.dashboardVersion }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Create PR or Publish release
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
version: pnpm run ci:version
|
||||
commit: 'chore: update versions'
|
||||
title: 'chore: update versions'
|
||||
publish: pnpm run release
|
||||
createGithubReleases: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Check Dashboard tag
|
||||
id: dashboard
|
||||
if: steps.changesets.outputs.hasChangesets == 'false'
|
||||
run: |
|
||||
DASHBOARD_VERSION=$(jq -r .version dashboard/package.json)
|
||||
GIT_TAG="${{ env.DASHBOARD_PACKAGE}}@$DASHBOARD_VERSION"
|
||||
if [ -z "$(git tag -l | grep $GIT_TAG)" ]; then
|
||||
echo "dashboardVersion=$DASHBOARD_VERSION" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
test:
|
||||
needs: version
|
||||
name: Dashboard
|
||||
if: needs.version.outputs.dashboardVersion != ''
|
||||
uses: ./.github/workflows/dashboard.yaml
|
||||
secrets: inherit
|
||||
|
||||
publish-vercel:
|
||||
name: Publish to Vercel
|
||||
needs:
|
||||
- test
|
||||
uses: ./.github/workflows/deploy-dashboard.yaml
|
||||
with:
|
||||
git_ref: ${{ github.ref_name }}
|
||||
environment: production
|
||||
secrets: inherit
|
||||
|
||||
publish-docker:
|
||||
name: Publish to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
- version
|
||||
- publish-vercel
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Add git tag
|
||||
run: |
|
||||
git tag "${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}"
|
||||
git push origin --tags
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
nhost/dashboard
|
||||
tags: |
|
||||
type=raw,value=latest,enable=true
|
||||
type=semver,pattern={{version}},value=v${{ needs.version.outputs.dashboardVersion }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=v${{ needs.version.outputs.dashboardVersion }}
|
||||
type=semver,pattern={{major}},value=v${{ needs.version.outputs.dashboardVersion }}
|
||||
type=sha
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push to Docker Hub
|
||||
uses: docker/build-push-action@v4
|
||||
timeout-minutes: 90
|
||||
with:
|
||||
context: .
|
||||
file: ./dashboard/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
TURBO_TOKEN=${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM=${{ env.TURBO_TEAM }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: true
|
||||
|
||||
bump-cli:
|
||||
name: Bump Dashboard version in the Nhost CLI
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- version
|
||||
- publish-docker
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: nhost/cli
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
fetch-depth: 0
|
||||
- name: Bump version in source code
|
||||
run: |
|
||||
IMAGE=$(echo ${{ env.DASHBOARD_PACKAGE }} | sed 's/@\(.\+\)\/\(.\+\)/\1\\\/\2/g')
|
||||
VERSION="${{ needs.version.outputs.dashboardVersion }}"
|
||||
EXPRESSION='s/"'$IMAGE':[0-9]\+\.[0-9]\+\.[0-9]\+"/"'$IMAGE':'$VERSION'"/g'
|
||||
find ./ -type f -exec sed -i -e $EXPRESSION {} \;
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
commit-message: 'chore: bump nhost/dashboard to ${{ needs.version.outputs.dashboardVersion }}'
|
||||
branch: bump-dashboard-version
|
||||
delete-branch: true
|
||||
title: 'chore: bump nhost/dashboard to ${{ needs.version.outputs.dashboardVersion }}'
|
||||
body: |
|
||||
This PR bumps the Nhost Dashboard Docker image to version ${{ needs.version.outputs.dashboardVersion }}.
|
||||
199
.github/workflows/ci.yaml
vendored
199
.github/workflows/ci.yaml
vendored
@@ -1,199 +0,0 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'assets/**'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths-ignore:
|
||||
- 'assets/**'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: nhost
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NHOST_TEST_DASHBOARD_URL: ${{ vars.NHOST_TEST_DASHBOARD_URL }}
|
||||
NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }}
|
||||
NHOST_TEST_ORGANIZATION_NAME: ${{ vars.NHOST_TEST_ORGANIZATION_NAME }}
|
||||
NHOST_TEST_ORGANIZATION_SLUG: ${{ vars.NHOST_TEST_ORGANIZATION_SLUG }}
|
||||
NHOST_TEST_PERSONAL_ORG_SLUG: ${{ vars.NHOST_TEST_PERSONAL_ORG_SLUG }}
|
||||
NHOST_TEST_PROJECT_SUBDOMAIN: ${{ vars.NHOST_TEST_PROJECT_SUBDOMAIN }}
|
||||
NHOST_PRO_TEST_PROJECT_NAME: ${{ vars.NHOST_PRO_TEST_PROJECT_NAME }}
|
||||
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 }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build @nhost packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# * Install Node and dependencies. Package downloads will be cached for the next jobs.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
BUILD: 'all'
|
||||
- name: Check if the pnpm lockfile changed
|
||||
id: changed-lockfile
|
||||
uses: tj-actions/changed-files@v37
|
||||
with:
|
||||
files: pnpm-lock.yaml
|
||||
# * Determine a pnpm filter argument for packages that have been modified.
|
||||
# * If the lockfile has changed, we don't filter anything in order to run all the e2e tests.
|
||||
- name: filter packages
|
||||
id: filter-packages
|
||||
if: steps.changed-lockfile.outputs.any_changed != 'true' && github.event_name == 'pull_request'
|
||||
run: echo "filter=${{ format('--filter=...[origin/{0}]', github.base_ref) }}" >> $GITHUB_OUTPUT
|
||||
# * List packagesthat has an `e2e` script, except the root, and return an array of their name and path
|
||||
# * In a PR, only include packages that have been modified, and their dependencies
|
||||
- name: List examples with an e2e script
|
||||
id: set-matrix
|
||||
run: |
|
||||
PACKAGES=$(pnpm recursive list --depth -1 --parseable --filter='!nhost-root' ${{ steps.filter-packages.outputs.filter }} \
|
||||
| xargs -I@ realpath --relative-to=$PWD @ \
|
||||
| xargs -I@ jq "if (.scripts.e2e | length) != 0 then {name: .name, path: \"@\"} else null end" @/package.json \
|
||||
| awk "!/null/" \
|
||||
| jq -c --slurp 'map(select(length > 0))')
|
||||
echo "matrix=$PACKAGES" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
|
||||
unit:
|
||||
name: Unit tests
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
# * Run every `test` script in the workspace . Dependencies build is cached by Turborepo
|
||||
- name: Run unit tests
|
||||
run: pnpm run test:all
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: '**/coverage/coverage-final.json'
|
||||
name: codecov-umbrella
|
||||
- name: Create summary
|
||||
run: |
|
||||
echo '### Code coverage' >> $GITHUB_STEP_SUMMARY
|
||||
echo 'Visit [codecov](https://app.codecov.io/gh/nhost/nhost/) to see the code coverage reports' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Enforce Prettier formatting in dashboard
|
||||
working-directory: ./dashboard
|
||||
run: pnpm prettier --check "./**/*.tsx" --config prettier.config.js
|
||||
# * Run every `lint` script in the workspace . Dependencies build is cached by Turborepo
|
||||
- name: Lint
|
||||
run: pnpm run lint:all
|
||||
- name: Audit for vulnerabilities
|
||||
run: pnpx audit-ci --config ./audit-ci.jsonc
|
||||
|
||||
e2e:
|
||||
name: 'E2E (Package: ${{ matrix.package.path }})'
|
||||
needs: build
|
||||
if: ${{ needs.build.outputs.matrix != '[]' && needs.build.outputs.matrix != '' }}
|
||||
strategy:
|
||||
# * Don't cancel other matrices when one fails
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.build.outputs.matrix) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
# * Build Dashboard image to test it locally
|
||||
- name: Build Dashboard local image
|
||||
if: matrix.package.path == 'dashboard'
|
||||
run: |
|
||||
docker build -t nhost/dashboard:0.0.0-dev -f ${{ matrix.package.path }}/Dockerfile .
|
||||
mkdir -p nhost-test-project
|
||||
# * Install Nhost CLI if a `nhost/config.yaml` file is found
|
||||
- name: Install Nhost CLI
|
||||
if: hashFiles(format('{0}/nhost/config.yaml', matrix.package.path)) != '' && matrix.package.path != 'dashboard'
|
||||
uses: ./.github/actions/nhost-cli
|
||||
# * Install Nhost CLI to test Dashboard locally
|
||||
- name: Install Nhost CLI (Local Dashboard tests)
|
||||
timeout-minutes: 5
|
||||
if: matrix.package.path == 'dashboard'
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
init: 'true' # Initialize the application
|
||||
start: 'true' # Start the application
|
||||
path: ./nhost-test-project
|
||||
wait: 'true' # Wait until the application is ready
|
||||
dashboard-image: 'nhost/dashboard:0.0.0-dev'
|
||||
- name: Fetch Dashboard Preview URL
|
||||
id: fetch-dashboard-preview-url
|
||||
uses: zentered/vercel-preview-url@v1.1.9
|
||||
if: github.ref_name != 'main'
|
||||
env:
|
||||
VERCEL_TOKEN: ${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
GITHUB_REF: ${{ github.ref_name }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
with:
|
||||
vercel_team_id: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
vercel_project_id: ${{ secrets.DASHBOARD_STAGING_VERCEL_PROJECT_ID }}
|
||||
vercel_state: BUILDING,READY,INITIALIZING
|
||||
- name: Set Dashboard Preview URL
|
||||
if: steps.fetch-dashboard-preview-url.outputs.preview_url != ''
|
||||
run: echo "NHOST_TEST_DASHBOARD_URL=https://${{ steps.fetch-dashboard-preview-url.outputs.preview_url }}" >> $GITHUB_ENV
|
||||
# * Run the `ci` script of the current package of the matrix. Dependencies build is cached by Turborepo
|
||||
- name: Run e2e tests
|
||||
timeout-minutes: 20
|
||||
run: pnpm --filter="${{ matrix.package.name }}" run e2e
|
||||
# * Run the `e2e-local` script of the dashboard
|
||||
- name: Run Local Dashboard e2e tests
|
||||
if: matrix.package.path == 'dashboard'
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
pnpm --filter="${{ matrix.package.name }}" run e2e-local
|
||||
|
||||
- name: Stop Nhost CLI
|
||||
if: matrix.package.path == 'dashboard'
|
||||
working-directory: ./nhost-test-project
|
||||
run: nhost down
|
||||
- id: file-name
|
||||
if: ${{ failure() }}
|
||||
name: Transform package name into a valid file name
|
||||
run: |
|
||||
PACKAGE_FILE_NAME=$(echo "${{ matrix.package.name }}" | sed 's/@//g; s/\//-/g')
|
||||
echo "fileName=$PACKAGE_FILE_NAME" >> $GITHUB_OUTPUT
|
||||
# * Run this step only if the previous step failed, and Playwright generated a report
|
||||
- name: Upload Playwright Report
|
||||
if: ${{ failure() && hashFiles(format('{0}/playwright-report/**', matrix.package.path)) != ''}}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-${{ steps.file-name.outputs.fileName }}
|
||||
path: ${{format('{0}/playwright-report/**', matrix.package.path)}}
|
||||
93
.github/workflows/ci_create_release.yaml
vendored
Normal file
93
.github/workflows/ci_create_release.yaml
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: "ci: create release"
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.title, 'release(')
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: read
|
||||
actions: write
|
||||
|
||||
steps:
|
||||
- name: "Check out repository"
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
install_url: "https://releases.nixos.org/nix/nix-2.28.4/install"
|
||||
install_options: "--no-daemon"
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
sandbox = false
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
substituters = https://cache.nixos.org/?priority=40 s3://nhost-nix-cache?region=eu-central-1&priority=50
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Restore and save Nix store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-cliff-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-cliff-${{ runner.os }}-${{ runner.arch }}}-
|
||||
gc-max-store-size-linux: 2G
|
||||
purge: true
|
||||
purge-prefixes: nix-cliff-
|
||||
purge-created: 0
|
||||
purge-last-accessed: 0
|
||||
purge-primary-key: never
|
||||
|
||||
- name: "Extract project and version from PR title"
|
||||
id: extract
|
||||
run: |
|
||||
TITLE="${{ github.event.pull_request.title }}"
|
||||
|
||||
PROJECT=$(echo "${TITLE}" | sed 's/release(\([^)]*\)).*/\1/')
|
||||
if [ -z "$PROJECT" ]; then
|
||||
echo "Error: Could not extract project name from PR title"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$(echo "${TITLE}" | sed 's/.*release([^)]*):\W*\(.*\).*/\1/')
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: Could not extract version from PR title"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROJECT_NAME=$(make release-tag-name)
|
||||
|
||||
echo "project=$PROJECT" >> $GITHUB_OUTPUT
|
||||
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "tag=$PROJECT_NAME@$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Get unreleased changelog content"
|
||||
id: changelog
|
||||
run: |
|
||||
cd ${{ steps.extract.outputs.project }}
|
||||
CHANGELOG_CONTENT=$(nix develop .#cliff -c make changelog-get-unreleased)
|
||||
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CHANGELOG_CONTENT" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Create GitHub Release"
|
||||
run: |
|
||||
gh release create "${{ steps.extract.outputs.tag }}" \
|
||||
--title "${{ steps.extract.outputs.tag }}" \
|
||||
--notes "${{ steps.changelog.outputs.content }}" \
|
||||
--target main
|
||||
env:
|
||||
# We need to use a PAT because GITHUB_TOKEN does not trigger workflows on releases
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }}
|
||||
69
.github/workflows/ci_release.yaml
vendored
Normal file
69
.github/workflows/ci_release.yaml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: "ci: release"
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
extract-project:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
project: ${{ steps.extract.outputs.project }}
|
||||
version: ${{ steps.extract.outputs.version }}
|
||||
steps:
|
||||
- name: "Extract project and version from tag"
|
||||
id: extract
|
||||
run: |
|
||||
TAG="${{ github.event.release.tag_name }}"
|
||||
|
||||
PROJECT=$(echo "${TAG}" | sed 's/@[^@]*$//')
|
||||
if [ -z "$PROJECT" ]; then
|
||||
echo "Error: Could not extract project name from tag"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$(echo "${TAG}" | sed 's/.*@//')
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: Could not extract version from tag"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "project=$PROJECT" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Extracted project: $PROJECT, version: $VERSION"
|
||||
|
||||
dashboard:
|
||||
needs: extract-project
|
||||
if: needs.extract-project.outputs.project == '@nhost/dashboard'
|
||||
uses: ./.github/workflows/dashboard_release.yaml
|
||||
with:
|
||||
NAME: dashboard
|
||||
GIT_REF: ${{ github.sha }}
|
||||
VERSION: ${{ needs.extract-project.outputs.version }}
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
VERCEL_TEAM_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_VERCEL_PROJECT_ID }}
|
||||
VERCEL_DEPLOY_TOKEN: ${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_PRODUCTION }}
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
nhost-js:
|
||||
needs: extract-project
|
||||
if: needs.extract-project.outputs.project == '@nhost/nhost-js'
|
||||
uses: ./.github/workflows/wf_release_npm.yaml
|
||||
with:
|
||||
NAME: nhost-js
|
||||
PATH: packages/nhost-js
|
||||
VERSION: ${{ needs.extract-project.outputs.version }}
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_PRODUCTION }}
|
||||
90
.github/workflows/ci_update_changelog.yaml
vendored
Normal file
90
.github/workflows/ci_update_changelog.yaml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: "ci: update changelog"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
update-changelog:
|
||||
if: ${{ !startsWith(github.event.head_commit.message, 'release(') }}
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
project: [dashboard, packages/nhost-js]
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
|
||||
steps:
|
||||
- name: "Check out repository"
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
install_url: "https://releases.nixos.org/nix/nix-2.28.4/install"
|
||||
install_options: "--no-daemon"
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
sandbox = false
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
substituters = https://cache.nixos.org/?priority=40 s3://nhost-nix-cache?region=eu-central-1&priority=50
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Restore and save Nix store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-cliff-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-cliff-${{ runner.os }}-${{ runner.arch }}}-
|
||||
gc-max-store-size-linux: 2G
|
||||
purge: true
|
||||
purge-prefixes: nix-cliff-${{ runner.os }}-
|
||||
purge-created: 0
|
||||
purge-last-accessed: 0
|
||||
purge-primary-key: never
|
||||
|
||||
- name: "Get next version"
|
||||
id: version
|
||||
run: |
|
||||
cd ${{ matrix.project }}
|
||||
VERSION=$(nix develop .\#cliff -c make changelog-next-version)
|
||||
if git tag | grep -q "${{ matrix.project }}@$VERSION"; then
|
||||
echo "Tag ${{ matrix.project }}/$VERSION already exists, skipping release preparation"
|
||||
else
|
||||
echo "Tag does not exist, proceeding with release preparation"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: "Update changelog"
|
||||
if: steps.version.outputs.version != ''
|
||||
run: |
|
||||
cd ${{ matrix.project }}
|
||||
nix develop .\#cliff -c make changelog-update
|
||||
|
||||
- name: "Create Pull Request"
|
||||
if: steps.version.outputs.version != ''
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "release(${{ matrix.project }}): ${{ steps.version.outputs.version }}"
|
||||
title: "release(${{ matrix.project }}): ${{ steps.version.outputs.version }}"
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||
body: |
|
||||
Automated release preparation for ${{ matrix.project }} version ${{ steps.version.outputs.version }}
|
||||
|
||||
Changes:
|
||||
- Updated CHANGELOG.md
|
||||
branch: release/${{ matrix.project }}
|
||||
delete-branch: true
|
||||
labels: |
|
||||
release,${{ matrix.project }}
|
||||
78
.github/workflows/codegen_checks.yaml
vendored
Normal file
78
.github/workflows/codegen_checks.yaml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: "codegen: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/codegen_checks.yaml'
|
||||
|
||||
# common build
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'nixops/**'
|
||||
- 'build/**'
|
||||
|
||||
# common go
|
||||
- '.golangci.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'vendor/**'
|
||||
|
||||
# codegen
|
||||
- 'tools/codegen/**'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "github.event_name: ${{ github.event_name }}"
|
||||
echo "github.event.pull_request.author_association: ${{ github.event.pull_request.author_association }}"
|
||||
- name: "This task will run and fail if user has no permissions and label safe_to_test isn't present"
|
||||
if: "github.event_name == 'pull_request_target' && ! ( contains(github.event.pull_request.labels.*.name, 'safe_to_test') || contains(fromJson('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.pull_request.author_association) )"
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
tests:
|
||||
uses: ./.github/workflows/wf_check.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: codegen
|
||||
PATH: tools/codegen
|
||||
GIT_REF: ${{ github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: codegen
|
||||
PATH: tools/codegen
|
||||
GIT_REF: ${{ github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-permissions
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: |
|
||||
safe_to_test
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe_to_test')
|
||||
49
.github/workflows/dashboard.yaml
vendored
49
.github/workflows/dashboard.yaml
vendored
@@ -1,49 +0,0 @@
|
||||
name: 'Dashboard'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: nhost
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Build the application
|
||||
run: pnpm build:dashboard
|
||||
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Run tests
|
||||
run: pnpm test:dashboard
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- run: pnpm lint:dashboard
|
||||
134
.github/workflows/dashboard_checks.yaml
vendored
Normal file
134
.github/workflows/dashboard_checks.yaml
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
name: "dashboard: check and build"
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/wf_build_artifacts.yaml'
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/dashboard_checks.yaml'
|
||||
|
||||
# common build
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'nixops/**'
|
||||
- 'build/**'
|
||||
|
||||
# common javascript
|
||||
- ".npmrc"
|
||||
- ".prettierignore"
|
||||
- ".prettierrc.js"
|
||||
- "audit-ci.jsonc"
|
||||
- "package.json"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "pnpm-lock.yaml"
|
||||
- "turbo.json"
|
||||
|
||||
# dashboard
|
||||
- "dashboard/**"
|
||||
|
||||
# nhost-js
|
||||
- packages/nhost-js/**
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "github.event_name: ${{ github.event_name }}"
|
||||
echo "github.event.pull_request.author_association: ${{ github.event.pull_request.author_association }}"
|
||||
- name: "This task will run and fail if user has no permissions and label safe_to_test isn't present"
|
||||
if: "github.event_name == 'pull_request_target' && ! ( contains(github.event.pull_request.labels.*.name, 'safe_to_test') || contains(fromJson('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.pull_request.author_association) )"
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
deploy-vercel:
|
||||
uses: ./.github/workflows/wf_deploy_vercel.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: dashboard
|
||||
GIT_REF: ${{ github.sha }}
|
||||
ENVIRONMENT: preview
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
VERCEL_TEAM_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_STAGING_VERCEL_PROJECT_ID }}
|
||||
VERCEL_DEPLOY_TOKEN: ${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
|
||||
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: dashboard
|
||||
PATH: dashboard
|
||||
GIT_REF: ${{ github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: true
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
|
||||
tests:
|
||||
uses: ./.github/workflows/wf_check.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
- build_artifacts
|
||||
with:
|
||||
NAME: dashboard
|
||||
PATH: dashboard
|
||||
GIT_REF: ${{ github.sha }}
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
|
||||
e2e_staging:
|
||||
uses: ./.github/workflows/wf_dashboard_e2e_staging.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
- deploy-vercel
|
||||
- build_artifacts
|
||||
with:
|
||||
NAME: dashboard
|
||||
PATH: dashboard
|
||||
GIT_REF: ${{ 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 }}
|
||||
NHOST_TEST_ORGANIZATION_SLUG: ${{ vars.NHOST_TEST_ORGANIZATION_SLUG }}
|
||||
NHOST_TEST_PERSONAL_ORG_SLUG: ${{ vars.NHOST_TEST_PERSONAL_ORG_SLUG }}
|
||||
NHOST_TEST_PROJECT_SUBDOMAIN: ${{ vars.NHOST_TEST_PROJECT_SUBDOMAIN }}
|
||||
NHOST_PRO_TEST_PROJECT_NAME: ${{ vars.NHOST_PRO_TEST_PROJECT_NAME }}
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
DASHBOARD_VERCEL_DEPLOY_TOKEN: ${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
DASHBOARD_VERCEL_TEAM_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
DASHBOARD_STAGING_VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_STAGING_VERCEL_PROJECT_ID }}
|
||||
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 }}
|
||||
PLAYWRIGHT_REPORT_ENCRYPTION_KEY: ${{ secrets.PLAYWRIGHT_REPORT_ENCRYPTION_KEY }}
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-permissions
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: |
|
||||
safe_to_test
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe_to_test')
|
||||
106
.github/workflows/dashboard_release.yaml
vendored
Normal file
106
.github/workflows/dashboard_release.yaml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
name: 'dashboard: release'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
NAME:
|
||||
required: true
|
||||
type: string
|
||||
GIT_REF:
|
||||
required: true
|
||||
type: string
|
||||
VERSION:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID:
|
||||
required: true
|
||||
NIX_CACHE_PUB_KEY:
|
||||
required: true
|
||||
NIX_CACHE_PRIV_KEY:
|
||||
required: true
|
||||
VERCEL_TEAM_ID:
|
||||
required: true
|
||||
VERCEL_PROJECT_ID:
|
||||
required: true
|
||||
VERCEL_DEPLOY_TOKEN:
|
||||
required: true
|
||||
DISCORD_WEBHOOK:
|
||||
required: false
|
||||
GH_PAT:
|
||||
required: true
|
||||
DOCKER_USERNAME:
|
||||
required: true
|
||||
DOCKER_PASSWORD:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
deploy-vercel:
|
||||
uses: ./.github/workflows/wf_deploy_vercel.yaml
|
||||
with:
|
||||
NAME: dashboard
|
||||
GIT_REF: ${{ inputs.GIT_REF }}
|
||||
ENVIRONMENT: production
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
VERCEL_DEPLOY_TOKEN: ${{ secrets.VERCEL_DEPLOY_TOKEN }}
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
with:
|
||||
NAME: dashboard
|
||||
PATH: dashboard
|
||||
GIT_REF: ${{ inputs.GIT_REF }}
|
||||
VERSION: ${{ inputs.VERSION }}
|
||||
DOCKER: true
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
push-docker:
|
||||
uses: ./.github/workflows/wf_docker_push_image.yaml
|
||||
needs:
|
||||
- build_artifacts
|
||||
with:
|
||||
NAME: dashboard
|
||||
PATH: dashboard
|
||||
VERSION: ${{ inputs.VERSION }}
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
bump-cli:
|
||||
name: Bump Dashboard version in the Nhost CLI
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- push-docker
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: nhost/cli
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
|
||||
- name: Bump version in source code
|
||||
run: |
|
||||
find . -type f -exec sed -i 's/"nhost\/dashboard:[^"]*"/"nhost\/dashboard:${{ inputs.VERSION }}"/g' {} +
|
||||
|
||||
- name: "Create Pull Request"
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
title: "chore: bump nhost/dashboard to ${{ inputs.VERSION }}"
|
||||
commit-message: "chore: bump nhost/dashboard to ${{ inputs.VERSION }}"
|
||||
committer: GitHub <noreply@github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||
body: |
|
||||
This PR bumps the Nhost Dashboard Docker image to version ${{ needs.version.outputs.dashboardVersion }}.
|
||||
branch: bump-dashboard-version
|
||||
delete-branch: true
|
||||
22
.github/workflows/dashboard_release_staging.yaml
vendored
Normal file
22
.github/workflows/dashboard_release_staging.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: "dashboard: release staging"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy-vercel:
|
||||
uses: ./.github/workflows/wf_deploy_vercel.yaml
|
||||
with:
|
||||
NAME: dashboard
|
||||
GIT_REF: ${{ github.sha }}
|
||||
ENVIRONMENT: production
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
VERCEL_TEAM_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_STAGING_VERCEL_PROJECT_ID }}
|
||||
VERCEL_DEPLOY_TOKEN: ${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_STAGING }}
|
||||
58
.github/workflows/deploy-dashboard.yaml
vendored
58
.github/workflows/deploy-dashboard.yaml
vendored
@@ -1,58 +0,0 @@
|
||||
name: 'dashboard: release form'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
git_ref:
|
||||
type: string
|
||||
description: 'Branch, tag, or commit SHA'
|
||||
required: true
|
||||
|
||||
environment:
|
||||
type: choice
|
||||
description: 'Deployment environment'
|
||||
required: true
|
||||
default: staging
|
||||
options:
|
||||
- staging
|
||||
- production
|
||||
|
||||
workflow_call:
|
||||
inputs:
|
||||
git_ref:
|
||||
required: true
|
||||
type: string
|
||||
environment:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish-vercel:
|
||||
name: Publish to Vercel
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.git_ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
|
||||
- name: Setup Vercel CLI
|
||||
run: pnpm add -g vercel
|
||||
|
||||
- name: Trigger Vercel deployment
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ inputs.environment == 'production' && secrets.DASHBOARD_VERCEL_PROJECT_ID || secrets.DASHBOARD_STAGING_VERCEL_PROJECT_ID }}
|
||||
run: |
|
||||
echo "Deploying to: ${{ inputs.environment }}..."
|
||||
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
70
.github/workflows/docs_checks.yaml
vendored
Normal file
70
.github/workflows/docs_checks.yaml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: "docs: check and build"
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/dashboard_checks.yaml'
|
||||
|
||||
# common build
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'nixops/**'
|
||||
- 'build/**'
|
||||
|
||||
# common javascript
|
||||
- ".npmrc"
|
||||
- ".prettierignore"
|
||||
- ".prettierrc.js"
|
||||
- "audit-ci.jsonc"
|
||||
- "package.json"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "pnpm-lock.yaml"
|
||||
- "turbo.json"
|
||||
|
||||
# docs
|
||||
- docs/**
|
||||
|
||||
# nhost-js
|
||||
- packages/nhost-js/**
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "github.event_name: ${{ github.event_name }}"
|
||||
echo "github.event.pull_request.author_association: ${{ github.event.pull_request.author_association }}"
|
||||
- name: "This task will run and fail if user has no permissions and label safe_to_test isn't present"
|
||||
if: "github.event_name == 'pull_request_target' && ! ( contains(github.event.pull_request.labels.*.name, 'safe_to_test') || contains(fromJson('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.pull_request.author_association) )"
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
|
||||
tests:
|
||||
uses: ./.github/workflows/wf_check.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: docs
|
||||
PATH: docs
|
||||
GIT_REF: ${{ github.sha }}
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-permissions
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: |
|
||||
safe_to_test
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe_to_test')
|
||||
94
.github/workflows/examples_demos_checks.yaml
vendored
Normal file
94
.github/workflows/examples_demos_checks.yaml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: "examples/demos: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/examples_demos_checks.yaml'
|
||||
|
||||
# common build
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'nixops/**'
|
||||
- 'build/**'
|
||||
|
||||
# common go
|
||||
- '.golangci.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'vendor/**'
|
||||
|
||||
# codegen
|
||||
- 'tools/codegen/**'
|
||||
|
||||
# common javascript
|
||||
- ".npmrc"
|
||||
- ".prettierignore"
|
||||
- ".prettierrc.js"
|
||||
- "audit-ci.jsonc"
|
||||
- "package.json"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "pnpm-lock.yaml"
|
||||
- "turbo.json"
|
||||
|
||||
# nhpst-js
|
||||
- 'packages/nhost-js/**'
|
||||
|
||||
# demos
|
||||
- 'examples/demos/**'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "github.event_name: ${{ github.event_name }}"
|
||||
echo "github.event.pull_request.author_association: ${{ github.event.pull_request.author_association }}"
|
||||
- name: "This task will run and fail if user has no permissions and label safe_to_test isn't present"
|
||||
if: "github.event_name == 'pull_request_target' && ! ( contains(github.event.pull_request.labels.*.name, 'safe_to_test') || contains(fromJson('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.pull_request.author_association) )"
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
tests:
|
||||
uses: ./.github/workflows/wf_check.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: demos
|
||||
PATH: examples/demos
|
||||
GIT_REF: ${{ github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: demos
|
||||
PATH: examples/demos
|
||||
GIT_REF: ${{ github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-permissions
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: |
|
||||
safe_to_test
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe_to_test')
|
||||
94
.github/workflows/examples_guides_checks.yaml
vendored
Normal file
94
.github/workflows/examples_guides_checks.yaml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: "examples/guides: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/examples_guides_checks.yaml'
|
||||
|
||||
# common build
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'nixops/**'
|
||||
- 'build/**'
|
||||
|
||||
# common go
|
||||
- '.golangci.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'vendor/**'
|
||||
|
||||
# codegen
|
||||
- 'tools/codegen/**'
|
||||
|
||||
# common javascript
|
||||
- ".npmrc"
|
||||
- ".prettierignore"
|
||||
- ".prettierrc.js"
|
||||
- "audit-ci.jsonc"
|
||||
- "package.json"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "pnpm-lock.yaml"
|
||||
- "turbo.json"
|
||||
|
||||
# nhpst-js
|
||||
- 'packages/nhost-js/**'
|
||||
|
||||
# guides
|
||||
- 'examples/guides/**'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "github.event_name: ${{ github.event_name }}"
|
||||
echo "github.event.pull_request.author_association: ${{ github.event.pull_request.author_association }}"
|
||||
- name: "This task will run and fail if user has no permissions and label safe_to_test isn't present"
|
||||
if: "github.event_name == 'pull_request_target' && ! ( contains(github.event.pull_request.labels.*.name, 'safe_to_test') || contains(fromJson('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.pull_request.author_association) )"
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
tests:
|
||||
uses: ./.github/workflows/wf_check.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: guides
|
||||
PATH: examples/guides
|
||||
GIT_REF: ${{ github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: guides
|
||||
PATH: examples/guides
|
||||
GIT_REF: ${{ github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-permissions
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: |
|
||||
safe_to_test
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe_to_test')
|
||||
94
.github/workflows/examples_tutorials_checks.yaml
vendored
Normal file
94
.github/workflows/examples_tutorials_checks.yaml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: "examples/tutorials: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/examples_tutorials_checks.yaml'
|
||||
|
||||
# common build
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'nixops/**'
|
||||
- 'build/**'
|
||||
|
||||
# common go
|
||||
- '.golangci.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'vendor/**'
|
||||
|
||||
# codegen
|
||||
- 'tools/codegen/**'
|
||||
|
||||
# common javascript
|
||||
- ".npmrc"
|
||||
- ".prettierignore"
|
||||
- ".prettierrc.js"
|
||||
- "audit-ci.jsonc"
|
||||
- "package.json"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "pnpm-lock.yaml"
|
||||
- "turbo.json"
|
||||
|
||||
# nhpst-js
|
||||
- 'packages/nhost-js/**'
|
||||
|
||||
# tutorials
|
||||
- 'examples/tutorials/**'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "github.event_name: ${{ github.event_name }}"
|
||||
echo "github.event.pull_request.author_association: ${{ github.event.pull_request.author_association }}"
|
||||
- name: "This task will run and fail if user has no permissions and label safe_to_test isn't present"
|
||||
if: "github.event_name == 'pull_request_target' && ! ( contains(github.event.pull_request.labels.*.name, 'safe_to_test') || contains(fromJson('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.pull_request.author_association) )"
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
tests:
|
||||
uses: ./.github/workflows/wf_check.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: tutorials
|
||||
PATH: examples/tutorials
|
||||
GIT_REF: ${{ github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: tutorials
|
||||
PATH: examples/tutorials
|
||||
GIT_REF: ${{ github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-permissions
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: |
|
||||
safe_to_test
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe_to_test')
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
@@ -15,9 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Configure aws
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
|
||||
15
.github/workflows/labeler.yaml
vendored
15
.github/workflows/labeler.yaml
vendored
@@ -1,15 +0,0 @@
|
||||
name: 'Pull Request Labeler'
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: '${{ secrets.GH_PAT }}'
|
||||
sync-labels: ''
|
||||
91
.github/workflows/nhost-js_checks.yaml
vendored
Normal file
91
.github/workflows/nhost-js_checks.yaml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
name: "nhost-js: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/nhost-js_checks.yaml'
|
||||
|
||||
# common build
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'nixops/**'
|
||||
- 'build/**'
|
||||
|
||||
# common go
|
||||
- '.golangci.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'vendor/**'
|
||||
|
||||
# codegen
|
||||
- 'tools/codegen/**'
|
||||
|
||||
# common javascript
|
||||
- ".npmrc"
|
||||
- ".prettierignore"
|
||||
- ".prettierrc.js"
|
||||
- "audit-ci.jsonc"
|
||||
- "package.json"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "pnpm-lock.yaml"
|
||||
- "turbo.json"
|
||||
|
||||
# nhost-js
|
||||
- 'packages/nhost-js/**'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "github.event_name: ${{ github.event_name }}"
|
||||
echo "github.event.pull_request.author_association: ${{ github.event.pull_request.author_association }}"
|
||||
- name: "This task will run and fail if user has no permissions and label safe_to_test isn't present"
|
||||
if: "github.event_name == 'pull_request_target' && ! ( contains(github.event.pull_request.labels.*.name, 'safe_to_test') || contains(fromJson('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.pull_request.author_association) )"
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
tests:
|
||||
uses: ./.github/workflows/wf_check.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: nhost-js
|
||||
PATH: packages/nhost-js
|
||||
GIT_REF: ${{ github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: nhost-js
|
||||
PATH: packages/nhost-js
|
||||
GIT_REF: ${{ github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-permissions
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: |
|
||||
safe_to_test
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe_to_test')
|
||||
70
.github/workflows/nixops_checks.yaml
vendored
Normal file
70
.github/workflows/nixops_checks.yaml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: "nixops: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/nixops_checks.yaml'
|
||||
|
||||
# common build
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'nixops/**'
|
||||
- 'build/**'
|
||||
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "github.event_name: ${{ github.event_name }}"
|
||||
echo "github.event.pull_request.author_association: ${{ github.event.pull_request.author_association }}"
|
||||
- name: "This task will run and fail if user has no permissions and label safe_to_test isn't present"
|
||||
if: "github.event_name == 'pull_request_target' && ! ( contains(github.event.pull_request.labels.*.name, 'safe_to_test') || contains(fromJson('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.pull_request.author_association) )"
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
tests:
|
||||
uses: ./.github/workflows/wf_check.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: nixops
|
||||
PATH: nixops
|
||||
GIT_REF: ${{ github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: nixops
|
||||
PATH: nixops
|
||||
GIT_REF: ${{ github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: false
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-permissions
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: |
|
||||
safe_to_test
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe_to_test')
|
||||
79
.github/workflows/test-nhost-cli-action.yaml
vendored
79
.github/workflows/test-nhost-cli-action.yaml
vendored
@@ -1,79 +0,0 @@
|
||||
name: Test Nhost CLI action
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- '.github/actions/nhost-cli/**'
|
||||
- '!.github/actions/nhost-cli/**/*.md'
|
||||
|
||||
jobs:
|
||||
install:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
- name: should succeed running the nhost command
|
||||
run: nhost
|
||||
|
||||
start:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI and start the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
init: true
|
||||
start: true
|
||||
- name: should be running
|
||||
run: curl -sSf 'https://local.hasura.local.nhost.run/' > /dev/null
|
||||
|
||||
stop:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI, start and stop the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
init: true
|
||||
start: true
|
||||
stop: true
|
||||
- name: should have no live docker container
|
||||
run: |
|
||||
if [ -z "docker ps -q" ]; then
|
||||
echo "Some docker containers are still running"
|
||||
docker ps
|
||||
exit 1
|
||||
fi
|
||||
|
||||
config:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI and run the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
init: true
|
||||
version: v1.29.3
|
||||
start: true
|
||||
- name: should find the injected hasura-auth version
|
||||
run: |
|
||||
VERSION=$(curl -sSf 'https://local.auth.local.nhost.run/v1/version')
|
||||
EXPECTED_VERSION='{"version":"0.36.1"}'
|
||||
if [ "$VERSION" != "$EXPECTED_VERSION" ]; then
|
||||
echo "Expected version $EXPECTED_VERSION but got $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
version: v1.27.2
|
||||
- name: should find the correct version
|
||||
run: nhost --version | head -n 1 | grep v1.27.2 || exit 1
|
||||
131
.github/workflows/wf_build_artifacts.yaml
vendored
Normal file
131
.github/workflows/wf_build_artifacts.yaml
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
NAME:
|
||||
type: string
|
||||
required: true
|
||||
GIT_REF:
|
||||
type: string
|
||||
required: false
|
||||
VERSION:
|
||||
type: string
|
||||
required: true
|
||||
PATH:
|
||||
type: string
|
||||
required: true
|
||||
DOCKER:
|
||||
type: boolean
|
||||
required: true
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID:
|
||||
required: true
|
||||
NIX_CACHE_PUB_KEY:
|
||||
required: true
|
||||
NIX_CACHE_PRIV_KEY:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
artifacts:
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ inputs.PATH }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [blacksmith-4vcpu-ubuntu-2404-arm, blacksmith-2vcpu-ubuntu-2404]
|
||||
fail-fast: true
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 180
|
||||
|
||||
steps:
|
||||
- name: "Check out repository"
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ inputs.GIT_REF }}
|
||||
|
||||
- name: Configure aws
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
|
||||
aws-region: eu-central-1
|
||||
|
||||
- uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
install_url: "https://releases.nixos.org/nix/nix-2.28.4/install"
|
||||
install_options: "--no-daemon"
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
sandbox = false
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
substituters = https://cache.nixos.org/?priority=40 s3://nhost-nix-cache?region=eu-central-1&priority=50
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Restore and save Nix store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-${{ inputs.NAME }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ inputs.NAME }}-${{ runner.os }}-${{ runner.arch }}}-
|
||||
gc-max-store-size-linux: 2G
|
||||
purge: true
|
||||
purge-prefixes: nix-${{ inputs.NAME }}-
|
||||
purge-created: 0
|
||||
purge-last-accessed: 0
|
||||
purge-primary-key: never
|
||||
|
||||
# - name: "Verify if nixops is pre-built"
|
||||
# id: verify-nixops-build
|
||||
# run: |
|
||||
# export drvPath=$(make build-nixops-dry-run)
|
||||
# echo "Derivation path: $drvPath"
|
||||
# nix path-info --store s3://nhost-nix-cache\?region=eu-central-1 $drvPath \
|
||||
# || (echo "Wait until nixops is already built and cached and run again" && exit 1)
|
||||
# if: ${{ inputs.NAME != 'nixops' }}
|
||||
|
||||
- name: Compute common env vars
|
||||
id: vars
|
||||
run: |
|
||||
echo "VERSION=$(make get-version VER=${{ inputs.VERSION }})" >> $GITHUB_OUTPUT
|
||||
ARCH=$([ "${{ runner.arch }}" == "X64" ] && echo "x86_64" || echo "aarch64")
|
||||
echo "ARCH=${ARCH}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Build artifact"
|
||||
run: |
|
||||
make build
|
||||
zip -r result.zip result
|
||||
|
||||
- name: "Push artifact to artifact repository"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.NAME }}-artifact-${{ steps.vars.outputs.ARCH }}-${{ steps.vars.outputs.VERSION }}
|
||||
path: ${{ inputs.PATH }}/result.zip
|
||||
retention-days: 7
|
||||
|
||||
- name: "Build docker image"
|
||||
run: |
|
||||
sudo chmod 755 /run/containers
|
||||
sudo mkdir -p "/run/containers/$(id -u runner)"
|
||||
sudo chown runner: "/run/containers/$(id -u runner)"
|
||||
make build-docker-image
|
||||
if: ${{ ( inputs.DOCKER ) }}
|
||||
|
||||
- name: "Push docker image to artifact repository"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.NAME }}-docker-image-${{ steps.vars.outputs.ARCH }}-${{ steps.vars.outputs.VERSION }}
|
||||
path: ${{ inputs.PATH }}/result
|
||||
retention-days: 7
|
||||
if: ${{ ( inputs.DOCKER ) }}
|
||||
|
||||
- name: "Cache build"
|
||||
run: |
|
||||
nix store sign --key-file <(echo "${{ secrets.NIX_CACHE_PRIV_KEY }}") --all
|
||||
find /nix/store -maxdepth 1 -name "*-*" -type d | xargs -n 25 nix copy --to s3://nhost-nix-cache\?region=eu-central-1
|
||||
if: always()
|
||||
111
.github/workflows/wf_check.yaml
vendored
Normal file
111
.github/workflows/wf_check.yaml
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
NAME:
|
||||
type: string
|
||||
required: true
|
||||
PATH:
|
||||
type: string
|
||||
required: true
|
||||
GIT_REF:
|
||||
type: string
|
||||
required: false
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID:
|
||||
required: true
|
||||
NIX_CACHE_PUB_KEY:
|
||||
required: true
|
||||
NIX_CACHE_PRIV_KEY:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ inputs.PATH }}
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- name: "Check out repository"
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ inputs.GIT_REF }}
|
||||
|
||||
- name: Collect Workflow Telemetry
|
||||
uses: catchpoint/workflow-telemetry-action@v2
|
||||
with:
|
||||
comment_on_pr: false
|
||||
|
||||
- name: Configure aws
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
|
||||
aws-region: eu-central-1
|
||||
|
||||
- uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
install_url: "https://releases.nixos.org/nix/nix-2.28.4/install"
|
||||
install_options: "--no-daemon"
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
sandbox = false
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
substituters = https://cache.nixos.org/?priority=40 s3://nhost-nix-cache?region=eu-central-1&priority=50
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Restore and save Nix store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-${{ inputs.NAME }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ inputs.NAME }}-${{ runner.os }}-${{ runner.arch }}}-
|
||||
gc-max-store-size-linux: 2G
|
||||
purge: true
|
||||
purge-prefixes: nix-${{ inputs.NAME }}-
|
||||
purge-created: 0
|
||||
purge-last-accessed: 0
|
||||
purge-primary-key: never
|
||||
|
||||
# - name: "Verify if nixops is pre-built"
|
||||
# id: verify-nixops-build
|
||||
# run: |
|
||||
# export drvPath=$(make build-nixops-dry-run)
|
||||
# echo "Derivation path: $drvPath"
|
||||
# nix path-info --store s3://nhost-nix-cache\?region=eu-central-1 $drvPath \
|
||||
# || (echo "Wait until nixops is already built and cached and run again" && exit 1)
|
||||
# if: ${{ inputs.NAME != 'nixops' }}
|
||||
|
||||
- name: "Verify if we need to build"
|
||||
id: verify-build
|
||||
run: |
|
||||
export drvPath=$(make check-dry-run)
|
||||
echo "Derivation path: $drvPath"
|
||||
nix path-info --store s3://nhost-nix-cache\?region=eu-central-1 $drvPath \
|
||||
&& export BUILD_NEEDED=no \
|
||||
|| export BUILD_NEEDED=yes
|
||||
echo BUILD_NEEDED=$BUILD_NEEDED >> $GITHUB_OUTPUT
|
||||
echo DERIVATION_PATH=$drvPath >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Start containters for integration tests"
|
||||
run: |
|
||||
nix develop .\#${{ inputs.NAME }} -c make dev-env-up
|
||||
if: ${{ steps.verify-build.outputs.BUILD_NEEDED == 'yes' }}
|
||||
|
||||
- name: "Run checks"
|
||||
run: make check
|
||||
if: ${{ steps.verify-build.outputs.BUILD_NEEDED == 'yes' }}
|
||||
|
||||
- name: "Cache build"
|
||||
run: |
|
||||
nix store sign --key-file <(echo "${{ secrets.NIX_CACHE_PRIV_KEY }}") --all
|
||||
find /nix/store -maxdepth 1 -name "*-*" -type d | xargs -n 25 nix copy --to s3://nhost-nix-cache\?region=eu-central-1
|
||||
if: always()
|
||||
165
.github/workflows/wf_dashboard_e2e_staging.yaml
vendored
Normal file
165
.github/workflows/wf_dashboard_e2e_staging.yaml
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
NAME:
|
||||
type: string
|
||||
required: true
|
||||
PATH:
|
||||
type: string
|
||||
required: true
|
||||
GIT_REF:
|
||||
type: string
|
||||
required: false
|
||||
NHOST_TEST_DASHBOARD_URL:
|
||||
type: string
|
||||
required: true
|
||||
NHOST_TEST_PROJECT_NAME:
|
||||
type: string
|
||||
required: true
|
||||
NHOST_TEST_ORGANIZATION_NAME:
|
||||
type: string
|
||||
required: true
|
||||
NHOST_TEST_ORGANIZATION_SLUG:
|
||||
type: string
|
||||
required: true
|
||||
NHOST_TEST_PERSONAL_ORG_SLUG:
|
||||
type: string
|
||||
required: true
|
||||
NHOST_TEST_PROJECT_SUBDOMAIN:
|
||||
type: string
|
||||
required: true
|
||||
NHOST_PRO_TEST_PROJECT_NAME:
|
||||
type: string
|
||||
required: true
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID:
|
||||
required: true
|
||||
NIX_CACHE_PUB_KEY:
|
||||
required: true
|
||||
NIX_CACHE_PRIV_KEY:
|
||||
required: true
|
||||
DASHBOARD_VERCEL_DEPLOY_TOKEN:
|
||||
required: true
|
||||
DASHBOARD_VERCEL_TEAM_ID:
|
||||
required: true
|
||||
DASHBOARD_STAGING_VERCEL_PROJECT_ID:
|
||||
required: true
|
||||
NHOST_TEST_USER_EMAIL:
|
||||
required: true
|
||||
NHOST_TEST_USER_PASSWORD:
|
||||
required: true
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET:
|
||||
required: true
|
||||
NHOST_TEST_FREE_USER_EMAILS:
|
||||
required: true
|
||||
PLAYWRIGHT_REPORT_ENCRYPTION_KEY:
|
||||
required: true
|
||||
|
||||
env:
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NHOST_TEST_DASHBOARD_URL: ${{ inputs.NHOST_TEST_DASHBOARD_URL }}
|
||||
NHOST_TEST_PROJECT_NAME: ${{ inputs.NHOST_TEST_PROJECT_NAME }}
|
||||
NHOST_TEST_ORGANIZATION_NAME: ${{ inputs.NHOST_TEST_ORGANIZATION_NAME }}
|
||||
NHOST_TEST_ORGANIZATION_SLUG: ${{ inputs.NHOST_TEST_ORGANIZATION_SLUG }}
|
||||
NHOST_TEST_PERSONAL_ORG_SLUG: ${{ inputs.NHOST_TEST_PERSONAL_ORG_SLUG }}
|
||||
NHOST_TEST_PROJECT_SUBDOMAIN: ${{ inputs.NHOST_TEST_PROJECT_SUBDOMAIN }}
|
||||
NHOST_PRO_TEST_PROJECT_NAME: ${{ inputs.NHOST_PRO_TEST_PROJECT_NAME }}
|
||||
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 }}
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ inputs.PATH }}
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- name: "Check out repository"
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ inputs.GIT_REF }}
|
||||
|
||||
- name: Collect Workflow Telemetry
|
||||
uses: catchpoint/workflow-telemetry-action@v2
|
||||
with:
|
||||
comment_on_pr: false
|
||||
|
||||
- name: Configure aws
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
|
||||
aws-region: eu-central-1
|
||||
|
||||
- uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
install_url: "https://releases.nixos.org/nix/nix-2.28.4/install"
|
||||
install_options: "--no-daemon"
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
sandbox = false
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
substituters = https://cache.nixos.org/?priority=40 s3://nhost-nix-cache?region=eu-central-1&priority=50
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Restore and save Nix store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-${{ inputs.NAME }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ inputs.NAME }}-${{ runner.os }}-${{ runner.arch }}}-
|
||||
gc-max-store-size-linux: 2G
|
||||
purge: true
|
||||
purge-prefixes: nix-${{ inputs.NAME }}-
|
||||
purge-created: 0
|
||||
purge-last-accessed: 0
|
||||
purge-primary-key: never
|
||||
|
||||
- name: Start CLI
|
||||
run: |
|
||||
nix develop .\#dashboard -c make dev-env-cli-up
|
||||
|
||||
- name: Run e2e tests
|
||||
run: nix develop .\#dashboard -c pnpm e2e
|
||||
|
||||
- name: Run e2e onboarding tests
|
||||
run: nix develop .\#dashboard -c pnpm e2e:onboarding
|
||||
|
||||
- name: Run e2e local tests
|
||||
run: nix develop .\#dashboard -c pnpm e2e:local
|
||||
|
||||
- name: Encrypt Playwright report
|
||||
if: failure()
|
||||
run: |
|
||||
tar -czf playwright-report.tar.gz playwright-report/
|
||||
openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000 \
|
||||
-in playwright-report.tar.gz \
|
||||
-out playwright-report.tar.gz.enc \
|
||||
-k "${{ secrets.PLAYWRIGHT_REPORT_ENCRYPTION_KEY }}"
|
||||
rm playwright-report.tar.gz
|
||||
|
||||
- name: Upload encrypted Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: encrypted-playwright-report-${{ github.run_id }}
|
||||
path: dashboard/playwright-report.tar.gz.enc
|
||||
retention-days: 1
|
||||
|
||||
- name: "Cache build"
|
||||
run: |
|
||||
nix store sign --key-file <(echo "${{ secrets.NIX_CACHE_PRIV_KEY }}") --all
|
||||
find /nix/store -maxdepth 1 -name "*-*" -type d | xargs -n 25 nix copy --to s3://nhost-nix-cache\?region=eu-central-1
|
||||
if: always()
|
||||
134
.github/workflows/wf_deploy_vercel.yaml
vendored
Normal file
134
.github/workflows/wf_deploy_vercel.yaml
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
name: 'deploy to vercel'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
NAME:
|
||||
required: true
|
||||
type: string
|
||||
GIT_REF:
|
||||
required: true
|
||||
type: string
|
||||
ENVIRONMENT:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID:
|
||||
required: true
|
||||
NIX_CACHE_PUB_KEY:
|
||||
required: true
|
||||
NIX_CACHE_PRIV_KEY:
|
||||
required: true
|
||||
VERCEL_TEAM_ID:
|
||||
required: true
|
||||
VERCEL_PROJECT_ID:
|
||||
required: true
|
||||
VERCEL_DEPLOY_TOKEN:
|
||||
required: true
|
||||
DISCORD_WEBHOOK:
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
preview-url:
|
||||
description: "The preview URL from Vercel deployment"
|
||||
value: ${{ jobs.publish-vercel.outputs.preview-url }}
|
||||
|
||||
jobs:
|
||||
publish-vercel:
|
||||
name: Publish to Vercel
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
outputs:
|
||||
preview-url: ${{ steps.deploy.outputs.preview-url }} # Add this line
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
actions: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ inputs.GIT_REF }}
|
||||
|
||||
- name: Configure aws
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
|
||||
aws-region: eu-central-1
|
||||
|
||||
- uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
install_url: "https://releases.nixos.org/nix/nix-2.28.4/install"
|
||||
install_options: "--no-daemon"
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
sandbox = false
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
substituters = https://cache.nixos.org/?priority=40 s3://nhost-nix-cache?region=eu-central-1&priority=50
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Restore and save Nix store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-${{ inputs.NAME }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ inputs.NAME }}-${{ runner.os }}-${{ runner.arch }}}-
|
||||
gc-max-store-size-linux: 2G
|
||||
purge: true
|
||||
purge-prefixes: nix-${{ inputs.NAME }}-
|
||||
purge-created: 0
|
||||
purge-last-accessed: 0
|
||||
purge-primary-key: never
|
||||
|
||||
- name: Trigger Vercel deployment
|
||||
id: deploy
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
run: |
|
||||
TARGET_OPTS="--target=${{ inputs.ENVIRONMENT }}"
|
||||
echo "Deploying to: ${{ inputs.ENVIRONMENT }}..."
|
||||
nix develop .\#vercel -c \
|
||||
vercel pull --environment=${{ inputs.ENVIRONMENT }} --token=${{ secrets.VERCEL_DEPLOY_TOKEN }}
|
||||
nix develop .\#vercel -c \
|
||||
vercel build $TARGET_OPTS --token=${{ secrets.VERCEL_DEPLOY_TOKEN }}
|
||||
nix develop .\#vercel -c \
|
||||
vercel deploy $TARGET_OPTS --prebuilt --token=${{ secrets.VERCEL_DEPLOY_TOKEN }} | tee /tmp/vercel_output
|
||||
|
||||
PREVIEW_URL=$(cat /tmp/vercel_output)
|
||||
echo "\n🔗🔗🔗 Preview URL: $PREVIEW_URL"
|
||||
echo "preview-url=$PREVIEW_URL" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: "vercel-${{ inputs.NAME }}-${{ inputs.ENVIRONMENT }}"
|
||||
message: |
|
||||
# Vercel Deployment Info - ${{ inputs.NAME }}
|
||||
|
||||
* URL: ${{ steps.deploy.outputs.preview-url }}
|
||||
* Git Ref: `${{ inputs.GIT_REF }}`
|
||||
* Commit: `${{ github.event.pull_request.head.sha || github.sha }}`
|
||||
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target'
|
||||
|
||||
- name: Send Discord notification
|
||||
if: always()
|
||||
uses: ./.github/actions/discord-notification
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
title: "Deployed ${{ inputs.NAME }} to Vercel"
|
||||
description: |
|
||||
**Environment**: ${{ inputs.ENVIRONMENT }}
|
||||
**URL**: ${{ steps.deploy.outputs.preview-url }}
|
||||
**Triggered by**: ${{ github.actor }}
|
||||
**Status**: ${{ job.status }}
|
||||
|
||||
**Details**:
|
||||
- Git Ref: ${{ inputs.GIT_REF }}
|
||||
- Commit: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
color: ${{ job.status == 'success' && '5763719' || '15548997' }}
|
||||
|
||||
- run: rm -rf .vercel
|
||||
if: always()
|
||||
79
.github/workflows/wf_docker_push_image.yaml
vendored
Normal file
79
.github/workflows/wf_docker_push_image.yaml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
NAME:
|
||||
type: string
|
||||
required: true
|
||||
PATH:
|
||||
type: string
|
||||
required: true
|
||||
VERSION:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
secrets:
|
||||
DOCKER_USERNAME:
|
||||
required: true
|
||||
DOCKER_PASSWORD:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
push-to-registry:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ inputs.PATH }}
|
||||
|
||||
steps:
|
||||
- name: "Check out repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: "Compute common env vars"
|
||||
id: vars
|
||||
run: |
|
||||
echo "VERSION=$(make get-version VER=${{ inputs.VERSION }})" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Get artifacts"
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: ~/artifacts
|
||||
|
||||
- name: "Inspect artifacts"
|
||||
run: find ~/artifacts
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: "Push docker image to docker hub"
|
||||
run: |
|
||||
export NAME=${{ inputs.NAME }}
|
||||
export VERSION=${{ steps.vars.outputs.VERSION }}
|
||||
export CONTAINER_REGISTRY=nhost
|
||||
export CONTAINER_NAME=$CONTAINER_REGISTRY/$NAME
|
||||
|
||||
for ARCH in "x86_64" "aarch64"; do
|
||||
skopeo copy --insecure-policy \
|
||||
dir:/home/runner/artifacts/${{ inputs.NAME }}-docker-image-$ARCH-$VERSION \
|
||||
docker-daemon:$CONTAINER_NAME:$VERSION-$ARCH
|
||||
docker push $CONTAINER_NAME:$VERSION-$ARCH
|
||||
done
|
||||
|
||||
docker manifest create \
|
||||
$CONTAINER_NAME:$VERSION \
|
||||
--amend $CONTAINER_NAME:$VERSION-x86_64 \
|
||||
--amend $CONTAINER_NAME:$VERSION-aarch64
|
||||
|
||||
docker manifest push $CONTAINER_NAME:$VERSION
|
||||
113
.github/workflows/wf_release_npm.yaml
vendored
Normal file
113
.github/workflows/wf_release_npm.yaml
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
NAME:
|
||||
type: string
|
||||
required: true
|
||||
PATH:
|
||||
type: string
|
||||
required: true
|
||||
VERSION:
|
||||
type: string
|
||||
required: true
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
AWS_ACCOUNT_ID:
|
||||
required: true
|
||||
NIX_CACHE_PUB_KEY:
|
||||
required: true
|
||||
NIX_CACHE_PRIV_KEY:
|
||||
required: true
|
||||
DISCORD_WEBHOOK:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ inputs.PATH }}
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- name: "Check out repository"
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Configure aws
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
|
||||
aws-region: eu-central-1
|
||||
|
||||
- uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
install_url: "https://releases.nixos.org/nix/nix-2.28.4/install"
|
||||
install_options: "--no-daemon"
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
sandbox = false
|
||||
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
|
||||
substituters = https://cache.nixos.org/?priority=40 s3://nhost-nix-cache?region=eu-central-1&priority=50
|
||||
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Restore and save Nix store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: nix-${{ inputs.NAME }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ inputs.NAME }}-${{ runner.os }}-${{ runner.arch }}}-
|
||||
gc-max-store-size-linux: 2G
|
||||
purge: true
|
||||
purge-prefixes: nix-${{ inputs.NAME }}-
|
||||
purge-created: 0
|
||||
purge-last-accessed: 0
|
||||
purge-primary-key: never
|
||||
|
||||
- name: "Build package"
|
||||
run: make build
|
||||
|
||||
- name: "Copy build output"
|
||||
run: cp -r result/dist .
|
||||
|
||||
- name: "Set package version"
|
||||
run: |
|
||||
nix develop .#pnpm -c pnpm version ${{ inputs.VERSION }} --no-git-tag-version
|
||||
|
||||
- name: "Determine npm tag"
|
||||
id: npm-tag
|
||||
run: |
|
||||
if [[ "${{ inputs.VERSION }}" =~ (alpha|beta|dev|rc) ]]; then
|
||||
echo "tag=beta" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=latest" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: "Publish to npm"
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
nix develop .#pnpm -c pnpm publish --tag ${{ steps.npm-tag.outputs.tag }} --no-git-checks
|
||||
|
||||
- name: Send Discord notification
|
||||
if: always()
|
||||
uses: ./.github/actions/discord-notification
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
title: "Published ${{ inputs.NAME }}@${{ inputs.VERSION }} to npm"
|
||||
description: |
|
||||
**Status**: ${{ job.status }}
|
||||
**Tag**: ${{ steps.npm-tag.outputs.tag }}
|
||||
**Triggered by**: ${{ github.actor }}
|
||||
|
||||
**Details**:
|
||||
- Version: ${{ inputs.VERSION }}
|
||||
- Package: ${{ inputs.NAME }}
|
||||
color: ${{ job.status == 'success' && '5763719' || '15548997' }}
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -65,3 +65,9 @@ out/
|
||||
.direnv/
|
||||
|
||||
/.vscode/
|
||||
|
||||
result
|
||||
|
||||
.vitest
|
||||
|
||||
.claude
|
||||
|
||||
58
.golangci.yaml
Normal file
58
.golangci.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
version: "2"
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
linters:
|
||||
default: all
|
||||
settings:
|
||||
funlen:
|
||||
lines: 65
|
||||
disable:
|
||||
- canonicalheader
|
||||
- depguard
|
||||
- gomoddirectives
|
||||
- musttag
|
||||
- nlreturn
|
||||
- tagliatelle
|
||||
- varnamelen
|
||||
- wsl
|
||||
- noinlineerr
|
||||
- funcorder
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- funlen
|
||||
- ireturn
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- lll
|
||||
source: '^//go:generate '
|
||||
- linters:
|
||||
- gochecknoglobals
|
||||
text: Version is a global variable
|
||||
- linters:
|
||||
- ireturn
|
||||
- lll
|
||||
path: schema\.resolvers\.go
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- gofumpt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
- schema\.resolvers\.go
|
||||
8
.npmrc
8
.npmrc
@@ -1,2 +1,8 @@
|
||||
prefer-workspace-packages = true
|
||||
auto-install-peers = false
|
||||
auto-install-peers = true
|
||||
|
||||
# without this setting, pnpm breaks monorepos with multiple versions of the same package
|
||||
shared-workspace-lockfile = false
|
||||
|
||||
# do not enable back, this leads to unlisted dependencies being used
|
||||
hoist = false
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
## Requirements
|
||||
|
||||
### Node.js v18
|
||||
|
||||
_⚠️ Node.js v16 is also supported for the time being but support will be dropped in the near future_.
|
||||
### Node.js v20 or later
|
||||
|
||||
### [pnpm](https://pnpm.io/) package manager
|
||||
|
||||
|
||||
50
README.md
50
README.md
@@ -61,27 +61,34 @@ Visit [https://docs.nhost.io](http://docs.nhost.io) for the complete documentati
|
||||
|
||||
Since Nhost is 100% open source, you can self-host the whole Nhost stack. Check out the example [docker-compose file](https://github.com/nhost/nhost/tree/main/examples/docker-compose) to self-host Nhost.
|
||||
|
||||
## Sign In and Make a Graphql Request
|
||||
## Sign In and Make a GraphQL Request
|
||||
|
||||
Install the `@nhost/nhost-js` package and start build your app:
|
||||
Install the `@nhost/nhost-js` package and start building your app:
|
||||
|
||||
```jsx
|
||||
import { NhostClient } from '@nhost/nhost-js'
|
||||
```ts
|
||||
import { createClient } from '@nhost/nhost-js'
|
||||
|
||||
const nhost = new NhostClient({
|
||||
subdomain: '<your-subdomain>',
|
||||
region: '<your-region>'
|
||||
const nhost = createClient({
|
||||
subdomain: 'your-project',
|
||||
region: 'eu-central-1'
|
||||
})
|
||||
|
||||
await nhost.auth.signIn({ email: 'user@domain.com', password: 'userPassword' })
|
||||
await nhost.auth.signInEmailPassword({
|
||||
email: 'user@example.com',
|
||||
password: 'password123'
|
||||
})
|
||||
|
||||
await nhost.graphql.request(`{
|
||||
users {
|
||||
id
|
||||
displayName
|
||||
email
|
||||
}
|
||||
}`)
|
||||
await nhost.graphql.request({
|
||||
query: `
|
||||
query GetUsers {
|
||||
users {
|
||||
id
|
||||
displayName
|
||||
email
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
## Frontend Agnostic
|
||||
@@ -103,19 +110,8 @@ Nhost is frontend agnostic, which means Nhost works with all frontend frameworks
|
||||
|
||||
## Nhost Clients
|
||||
|
||||
- [JavaScript/TypeScript](https://docs.nhost.io/reference/javascript/nhost-js/nhost-client)
|
||||
- [JavaScript/TypeScript](https://docs.nhost.io/reference/javascript/nhost-js/main)
|
||||
- [Dart and Flutter](https://github.com/nhost/nhost-dart)
|
||||
- [React](https://docs.nhost.io/reference/react/nhost-client)
|
||||
- [Next.js](https://docs.nhost.io/reference/nextjs/nhost-client)
|
||||
- [Vue](https://docs.nhost.io/reference/vue/nhost-client)
|
||||
|
||||
## Integrations
|
||||
|
||||
- [Apollo](./integrations/apollo#nhostapollo)
|
||||
- [React Apollo](./integrations/react-apollo#nhostreact-apollo)
|
||||
- [React URQL](./integrations/react-urql#nhostreact-urql)
|
||||
- [Stripe GraphQL API](./integrations/stripe-graphql-js#nhoststripe-graphql-js)
|
||||
- [Google Translation GraphQL API](./integrations/google-translation#nhostgoogle-translation)
|
||||
|
||||
## Applications
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
// $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"]
|
||||
"allowlist": ["vue-template-compiler", { "id": "CVE-2025-48068", "path": "next" }]
|
||||
}
|
||||
|
||||
43
biome.json
Normal file
43
biome.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.2/schema.json",
|
||||
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
|
||||
"files": { "ignoreUnknown": false },
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 80
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"complexity": {
|
||||
"useLiteralKeys": "off"
|
||||
}
|
||||
},
|
||||
"includes": ["**", "!.next", "!node_modules"]
|
||||
},
|
||||
"javascript": { "formatter": { "quoteStyle": "double" }, "globals": [] },
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": { "source": { "organizeImports": "on" } }
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["**/*.svelte", "**/*.astro", "**/*.vue"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useConst": "off",
|
||||
"useImportType": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "off",
|
||||
"noUnusedImports": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
29
build/configs/README.md
Normal file
29
build/configs/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Configuration Files
|
||||
|
||||
This directory contains standardized configurations for various tools and frameworks used across the repository.
|
||||
|
||||
## Available Configurations
|
||||
|
||||
- [**TypeScript (`/tsconfig`)**](./tsconfig/README.md): Centralized TypeScript configurations for different project types
|
||||
|
||||
- Standard base configuration with strict type checking
|
||||
- Specialized configurations for libraries, frontend apps, and Node.js
|
||||
- Documented usage patterns and extension points
|
||||
|
||||
## Using the Configurations
|
||||
|
||||
Each configuration directory contains a README with specific instructions on how to use the configurations in your projects.
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Consistency**: All projects follow the same standards and best practices
|
||||
- **Maintainability**: Configuration changes can be made in one place and propagated to all projects
|
||||
- **Onboarding**: New projects can quickly adopt the standard configurations
|
||||
|
||||
## Adding New Configurations
|
||||
|
||||
When adding new centralized configurations:
|
||||
|
||||
1. Create a new subdirectory with an appropriate name
|
||||
2. Include a README.md explaining the configurations
|
||||
3. Document both the usage and the reasoning behind configuration choices
|
||||
58
build/configs/tsconfig/README.md
Normal file
58
build/configs/tsconfig/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# TypeScript Configurations
|
||||
|
||||
This directory contains centralized TypeScript configurations that can be extended by projects in the monorepo. Using centralized configurations ensures consistency across projects and makes it easier to maintain and update TypeScript settings.
|
||||
|
||||
## Base Configurations
|
||||
|
||||
- `base.json`: Core TypeScript settings used by all projects
|
||||
- `library.json`: Settings for libraries and SDK packages
|
||||
- `frontend.json`: Settings for frontend applications (React, Next.js)
|
||||
- `node.json`: Settings for Node.js applications and scripts
|
||||
- `vite.json`: Settings for Vite configuration files
|
||||
|
||||
## Usage
|
||||
|
||||
In your project's `tsconfig.json` file, extend the appropriate base configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../configs/tsconfig/frontend.json",
|
||||
"compilerOptions": {
|
||||
// Project-specific overrides here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Features
|
||||
|
||||
### Common Features
|
||||
|
||||
- Strict type checking
|
||||
- Modern ES features
|
||||
- Comprehensive linting rules
|
||||
- Proper module resolution
|
||||
|
||||
### Library Configuration
|
||||
|
||||
- Declaration file generation
|
||||
- Source maps
|
||||
- Composite project support
|
||||
|
||||
### Frontend Configuration
|
||||
|
||||
- JSX support
|
||||
- DOM typings
|
||||
- Bundler module resolution
|
||||
- Compatible with both React and Next.js
|
||||
- Configurable for specific framework needs
|
||||
|
||||
## Creating New Projects
|
||||
|
||||
When creating a new project:
|
||||
|
||||
1. Identify the appropriate base configuration for your project type
|
||||
2. Create a minimal `tsconfig.json` that extends the base configuration from the `configs/tsconfig` directory
|
||||
3. Add only project-specific customizations to your `tsconfig.json`
|
||||
|
||||
This approach ensures all projects follow the same standards while allowing for project-specific needs.
|
||||
34
build/configs/tsconfig/base.json
Normal file
34
build/configs/tsconfig/base.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "Base Configuration",
|
||||
"compilerOptions": {
|
||||
/* Environment and Features */
|
||||
"lib": ["ESNext"],
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"allowUnusedLabels": false,
|
||||
"allowUnreachableCode": false,
|
||||
|
||||
/* Module Resolution */
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
/* Advanced Options */
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"exclude": ["node_modules", "**/dist", "**/build"]
|
||||
}
|
||||
25
build/configs/tsconfig/frontend.json
Normal file
25
build/configs/tsconfig/frontend.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "Frontend Configuration",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
/* Frontend Specific */
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Module Resolution */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
|
||||
/* Additional Options */
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"incremental": true,
|
||||
|
||||
/* Next.js Compatibility (ignored by non-Next.js projects) */
|
||||
"plugins": []
|
||||
},
|
||||
"include": ["src/**/*", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules", "**/node_modules/*"]
|
||||
}
|
||||
30
build/configs/tsconfig/library.json
Normal file
30
build/configs/tsconfig/library.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "Library/SDK Configuration",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
/* Output Configuration */
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"noEmit": false,
|
||||
"composite": true,
|
||||
"importHelpers": true,
|
||||
|
||||
/* Library-specific */
|
||||
"moduleResolution": "node",
|
||||
|
||||
/* Types */
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/__tests__/**",
|
||||
"dist",
|
||||
"**/dist/*"
|
||||
]
|
||||
}
|
||||
23
build/configs/tsconfig/node.json
Normal file
23
build/configs/tsconfig/node.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "Node.js Configuration",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"target": "ES2022",
|
||||
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
/* Node-specific options */
|
||||
"sourceMap": true,
|
||||
|
||||
/* Types */
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": ["node_modules", "**/node_modules/*"]
|
||||
}
|
||||
14
build/configs/tsconfig/vite.json
Normal file
14
build/configs/tsconfig/vite.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "Vite Configuration",
|
||||
"extends": "./node.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
113
build/makefiles/general.makefile
Normal file
113
build/makefiles/general.makefile
Normal file
@@ -0,0 +1,113 @@
|
||||
PROJ_DIR=$(abspath .)
|
||||
PROJ=$(subst $(ROOT_DIR)/,,$(PROJ_DIR))
|
||||
NAME=$(notdir $(PROJ))
|
||||
|
||||
include $(ROOT_DIR)/build/makefiles/release.makefile
|
||||
|
||||
ifdef VER
|
||||
VERSION=$(shell echo $(VER) | sed -e 's/^v//g' -e 's/\//_/g')
|
||||
else
|
||||
VERSION=$(shell grep -oP 'version\s*=\s*"\K[^"]+' project.nix | head -n 1)
|
||||
endif
|
||||
|
||||
ifeq ($(shell uname -m),x86_64)
|
||||
ARCH?=x86_64
|
||||
else ifeq ($(shell uname -m),arm64)
|
||||
ARCH?=aarch64
|
||||
else ifeq ($(shell uname -m),aarch64)
|
||||
ARCH?=aarch64
|
||||
else
|
||||
ARCH?=FIXME-$(shell uname -m)
|
||||
endif
|
||||
|
||||
ifeq ($(shell uname -o),Darwin)
|
||||
OS?=darwin
|
||||
else
|
||||
OS?=linux
|
||||
endif
|
||||
|
||||
ifeq ($(CI),true)
|
||||
docker-build-options=--option system $(ARCH)-linux --extra-platforms ${ARCH}-linux
|
||||
endif
|
||||
|
||||
|
||||
.PHONY: help
|
||||
help: ## Show this help.
|
||||
@echo
|
||||
@awk 'BEGIN { \
|
||||
FS = "##"; \
|
||||
printf "Usage: make \033[36m<target>\033[0m\n"} \
|
||||
/^[a-zA-Z_-]+%?:.*?##/ { printf " \033[36m%-38s\033[0m %s\n", $$1, $$2 } ' \
|
||||
$(MAKEFILE_LIST)
|
||||
|
||||
.PHONY: print-vars
|
||||
print-vars: ## print all variables
|
||||
@$(foreach V,$(sort $(.VARIABLES)), \
|
||||
$(if $(filter-out environment% default automatic, \
|
||||
$(origin $V)),$(info $V=$($V) ($(value $V)))))
|
||||
|
||||
|
||||
.PHONY: get-version
|
||||
get-version: ## Return version
|
||||
@sed -i '/^\s*version = "0.0.0-dev";/s//version = "${VERSION}";/' project.nix
|
||||
@sed -i '/^\s*created = "1970-.*";/s//created = "${shell date --utc '+%Y-%m-%dT%H:%M:%SZ'}";/' project.nix
|
||||
@echo $(VERSION)
|
||||
|
||||
|
||||
.PHONY: check
|
||||
check: ## Run nix flake check
|
||||
nix build \
|
||||
--print-build-logs \
|
||||
.\#checks.$(ARCH)-$(OS).$(NAME)
|
||||
|
||||
|
||||
.PHONY: check-dry-run
|
||||
check-dry-run: ## Returns the derivation of the check
|
||||
@nix build \
|
||||
--dry-run \
|
||||
--json \
|
||||
.\#checks.$(ARCH)-$(OS).$(NAME) | jq -r '.[].outputs.out'
|
||||
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build application and places the binary under ./result/bin
|
||||
nix build \
|
||||
--print-build-logs \
|
||||
.\#packages.$(ARCH)-$(OS).$(NAME)
|
||||
|
||||
|
||||
.PHONY: build-dry-run
|
||||
build-dry-run: ## Run nix flake check
|
||||
@nix build \
|
||||
--dry-run \
|
||||
--json \
|
||||
.\#packages.$(ARCH)-$(OS).$(NAME) | jq -r '.[].outputs.out'
|
||||
|
||||
|
||||
.PHONY: build-nixops-dry-run
|
||||
build-nixops-dry-run: ## Checks if nixops needs to be rebuilt
|
||||
@nix build \
|
||||
--dry-run \
|
||||
--json \
|
||||
.\#packages.$(ARCH)-$(OS).nixops | jq -r '.[].outputs.out'
|
||||
|
||||
|
||||
.PHONY: build-docker-image
|
||||
build-docker-image: ## Build docker container for native architecture
|
||||
nix build $(docker-build-options) --show-trace \
|
||||
.\#packages.$(ARCH)-linux.$(NAME)-docker-image \
|
||||
--print-build-logs
|
||||
nix develop \#skopeo -c \
|
||||
skopeo copy --insecure-policy dir:./result docker-daemon:$(NAME):$(VERSION)
|
||||
|
||||
|
||||
.PHONY: dev-env-up
|
||||
dev-env-up: _dev-env-build _dev-env-up ## Starts development environment
|
||||
|
||||
|
||||
.PHONY: dev-env-down
|
||||
dev-env-down: _dev-env-down ## Stops development environment
|
||||
|
||||
|
||||
.PHONY: dev-env-build
|
||||
dev-env-build: _dev-env-build ## Builds development environment
|
||||
30
build/makefiles/release.makefile
Normal file
30
build/makefiles/release.makefile
Normal file
@@ -0,0 +1,30 @@
|
||||
TAG_NAME?=$(NAME)
|
||||
TAG_PATTERN="^$(TAG_NAME)@\d+\.\d+\.\d+$$"
|
||||
|
||||
|
||||
.PHONY: changelog-init
|
||||
changelog-init: ## Initialize changelog using git-cliff
|
||||
@git cliff -u --tag-pattern "$(TAG_PATTERN)" --bump --tag="$(NAME)/$(VERSION)" --output CHANGELOG.md
|
||||
|
||||
.PHONY: changelog-next-version
|
||||
changelog-next-version: ## Get next version using git-cliff
|
||||
@git cliff -u --bumped-version --tag-pattern $(TAG_PATTERN) $(CLIFF_OPTS) | awk -F\@ '{print $$2}'
|
||||
|
||||
.PHONY: changelog-get-released
|
||||
changelog-get-released: ## Get changelog for the latest release using git-cliff
|
||||
@git cliff -l --bump --tag-pattern $(TAG_PATTERN) $(CLIFF_OPTS) --strip all
|
||||
|
||||
|
||||
.PHONY: changelog-get-unreleased
|
||||
changelog-get-unreleased: ## Get changelog for the following release using git-cliff
|
||||
@git cliff -u --bump --tag-pattern $(TAG_PATTERN) $(CLIFF_OPTS) --strip all
|
||||
|
||||
|
||||
.PHONY: changelog-update
|
||||
changelog-update: ## Update changelog using git-cliff
|
||||
@git cliff -u --bump --tag-pattern $(TAG_PATTERN) $(CLIFF_OPTS) --prepend CHANGELOG.md
|
||||
|
||||
|
||||
.PHONY: release-tag-name
|
||||
release-tag-name: ## Get the tag name for the current version
|
||||
@echo "$(TAG_NAME)"
|
||||
90
cliff.toml
Normal file
90
cliff.toml
Normal file
@@ -0,0 +1,90 @@
|
||||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
|
||||
[changelog]
|
||||
# A Tera template to be rendered for each release in the changelog.
|
||||
# See https://keats.github.io/tera/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits %}
|
||||
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
||||
{% if commit.breaking %}[**breaking**] {% endif %}\
|
||||
{{ commit.message | upper_first }}\
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
"""
|
||||
# Remove leading and trailing whitespaces from the changelog's body.
|
||||
trim = true
|
||||
# Render body even when there are no releases to process.
|
||||
render_always = true
|
||||
# An array of regex based postprocessors to modify the changelog.
|
||||
postprocessors = [
|
||||
# Replace the placeholder <REPO> with a URL.
|
||||
#{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
|
||||
]
|
||||
# render body even when there are no releases to process
|
||||
# render_always = true
|
||||
# output file path
|
||||
# output = "test.md"
|
||||
|
||||
[git]
|
||||
# Parse commits according to the conventional commits specification.
|
||||
# See https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# Exclude commits that do not match the conventional commits specification.
|
||||
filter_unconventional = false
|
||||
# Require all commits to be conventional.
|
||||
# Takes precedence over filter_unconventional.
|
||||
require_conventional = false
|
||||
# Split commits on newlines, treating each line as an individual commit.
|
||||
split_commits = false
|
||||
# An array of regex based parsers to modify commit messages prior to further processing.
|
||||
commit_preprocessors = [
|
||||
{ pattern = '^(\w+)\W*(\(\w+\))', replace = '${1}${2}' },
|
||||
]
|
||||
# Prevent commits that are breaking from being excluded by commit parsers.
|
||||
protect_breaking_commits = false
|
||||
# An array of regex based parsers for extracting data from the commit message.
|
||||
# Assigns commits to groups.
|
||||
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
||||
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
||||
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
||||
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
||||
{ message = "^release", skip = true },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(deps.*\\)", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
||||
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
||||
{ message = ".*", group = "<!-- 10 -->💼 Other" },
|
||||
]
|
||||
# Exclude commits that are not matched by any commit parser.
|
||||
filter_commits = false
|
||||
# An array of link parsers for extracting external references, and turning them into URLs, using regex.
|
||||
link_parsers = []
|
||||
# Include only the tags that belong to the current branch.
|
||||
use_branch_tags = false
|
||||
# Order releases topologically instead of chronologically.
|
||||
topo_order = false
|
||||
# Order releases topologically instead of chronologically.
|
||||
topo_order_commits = true
|
||||
# Order of commits in each group/release within the changelog.
|
||||
# Allowed values: newest, oldest
|
||||
sort_commits = "oldest"
|
||||
# Process submodules commits
|
||||
recurse_submodules = false
|
||||
@@ -25,4 +25,6 @@ NEXT_PUBLIC_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_TURNSTILE_SITE_KEY=FIXME
|
||||
|
||||
NEXT_PUBLIC_SOC2_REPORT_FILE_ID=
|
||||
@@ -76,6 +76,13 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
'jsx-a11y/label-has-associated-control': [
|
||||
2,
|
||||
{
|
||||
controlComponents: ['Input'],
|
||||
depth: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
4
dashboard/.gitignore
vendored
4
dashboard/.gitignore
vendored
@@ -54,4 +54,6 @@ tailwind.json
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
storageState.json
|
||||
e2e/.auth/*
|
||||
e2e/.auth/*
|
||||
|
||||
.vitest
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
link-workspace-packages = false
|
||||
auto-install-peers = false
|
||||
resolution-mode=highest
|
||||
@@ -1,8 +1,9 @@
|
||||
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 { NhostClient, NhostProvider } from '@nhost/nextjs';
|
||||
import { createClient } from '@nhost/nhost-js-beta';
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Buffer } from 'buffer';
|
||||
@@ -58,7 +59,9 @@ export const decorators = [
|
||||
</NhostApolloProvider>
|
||||
),
|
||||
(Story) => (
|
||||
<NhostProvider nhost={new NhostClient({ subdomain: 'local' })}>
|
||||
<NhostProvider
|
||||
nhost={createClient({ subdomain: 'local', region: 'local' })}
|
||||
>
|
||||
<Story />
|
||||
</NhostProvider>
|
||||
),
|
||||
|
||||
@@ -1,5 +1,153 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 2.37.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- cc98f33: fix: rename filename typo
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 25c0ffa: fix (dashboard): Parse tablename correctly into SQL query
|
||||
- 8812d9d: feat (dsashboard): Simplyfy column and row controls in database view
|
||||
|
||||
## 2.36.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- a30da08: feat (dashboard): add custom types to column types
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 73a7ba8: fix (dashboard): Show errors in row permission rule form
|
||||
- 397bfc9: fix (dashboard): Parse foreign key relations correctly
|
||||
- 2f4b376: fix (dashboard): allow permission variables with in operator
|
||||
- 88836f3: fix (dashboard): use correct fallback endpoint for migration in the CLI
|
||||
- ba3c49e: fix (dashboard): Show nested relationships in row permissions
|
||||
- 92e71a6: fix: minor fixes to csp
|
||||
- 81716d9: fix (dashboard): Show validation error on save when editing database columns
|
||||
|
||||
## 2.35.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 7633d04: feat (dashbord): Allow composite primary keys
|
||||
- c4f383f: fix: dashboard: don't allow for upgrading to starter
|
||||
- 4c6400f: fix: handle redirect to verify email page if sign in with github
|
||||
- 7f0db21: feat: added entraid support
|
||||
- 412692c: chore (dashboard): Turn on strictNullChecks config
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1708578: fix (dashboard): Update navbar after org and project operations
|
||||
- 34ede5c: fix: enable csp again
|
||||
- 96228df: chore (dashboard): update nhost-js to the latest version
|
||||
- d8c5117: fix (dashboard): Allow creating tables without primary key
|
||||
- 89f6fe6: chore (docker-example): update dashboard image version
|
||||
- e8a3789: fix (dashboard): scroll to active element in navbar when navigating
|
||||
|
||||
## 2.34.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 7eb9539: feat (dashboard): Allow upgrading free organizations
|
||||
- 129ec1e: feat: dashboard: new onboarding
|
||||
- 59249e5: fix: elevate permissions in password reset
|
||||
- 5e9ddb4: fix: show Run service name in logs page
|
||||
- 4ffff86: fix (dashboard): Disable settings pages when config server env variable is not set
|
||||
- b8cb491: fix: update dependencies to fix vulnerabilities
|
||||
- 5565451: fix: support page, can scroll all the way down in Chrome for iOS
|
||||
- f7d7080: chore: dashboard: add gtag
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 181c0ab: fix (dashboard): Fix upgrade project e2e tests
|
||||
- 56c87da: fix (dashboard): Use the correct http method when conneting to new github
|
||||
- 00132bd: fix (dashboard): Clear isSigningOut variable on Signin page
|
||||
- 66e0cc8: fix (dashboard): Check if user is logged in before redirecting
|
||||
- 9c0a118: chore (dashboard): Add RetryLink to ApolloClient
|
||||
- df6b85e: fix (dashboard): fix password reset redirect url
|
||||
- ec24567: fix (dashboard): Add content-type header
|
||||
- 57b2615: chore (dashboard): refactor redirect behaviour
|
||||
- cffa161: fix (dashboard): disable settings in the header when self-hosting
|
||||
- 85316e8: fix (dashboard): Remove second loading indicator on projects page
|
||||
- 47ab341: fix (dashboard): Fix announcement layout when title is too short
|
||||
|
||||
## 2.33.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- aee9a80: chore: update typescript version to the latest
|
||||
- 5ef3f76: chore (dashboard): Use the new SDK in the Dashboard
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9ed8ce8: fix (dashboard): Request new Mfa ticket after an invalid totp when signing in
|
||||
- fd3b5c7: fix (dashboard): Limit new project's name to a maximum of 32 charachters in E2E tests
|
||||
|
||||
## 2.32.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 736862c: fix: update link to base directory docs in git settings
|
||||
- ea99fb3: chore: dashboard: improve messaging when git connected
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d738884: chore (dashboard): Add link about antivirus integration
|
||||
|
||||
## 2.31.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 39b10a2: feat (dashboard): Add multi-factor authentication
|
||||
- 4b84780: feat (dashboard): Add Webauthn to dashboard
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 61eb6cd: fix (dashboard): Fix update project e2e test
|
||||
- @nhost/react-apollo@18.0.0
|
||||
- @nhost/nextjs@2.2.8
|
||||
|
||||
## 2.30.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- f6947a2: fix: fetch job-backup services logs using Live filter
|
||||
- 44a3e6b: fix: collapsed main navigation sidebar overlaps mobile navbar
|
||||
- 99b78f1: feat: dashboard: add download button for soc2 report
|
||||
- 9acae7d: fix: e2e tests, stop on error when refreshing metadata
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 31e636a: fix (dashboard): Use the correct payload to reset metadata before the e2e tests
|
||||
|
||||
## 2.29.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- c97b43f: fix: update vite to address vulnerability audit
|
||||
- a0931e2: fix: improve logs time range and filter selection
|
||||
- c0635ae: feat (dashboard): Add information about that free organization cannot be upgraded.
|
||||
- e87505c: fix: can downsize postgres storage capacity using local dashboard
|
||||
|
||||
## 2.28.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8552678: feat: dashboard: add additional events to segment
|
||||
- 0bf2808: chore: refresh metadata before end-to-end tests
|
||||
- 72a365c: fix: correct graphql page roles dropdown's source
|
||||
- cef6471: fix: dashboard: add anonid to user's metadata
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d9eb906: fix: update vite and nextjs because of vulnerability
|
||||
- 233232b: feat (dashboard): improve Upgrade project dialog
|
||||
- Updated dependencies [d9eb906]
|
||||
- @nhost/nextjs@2.2.7
|
||||
- @nhost/react-apollo@17.0.4
|
||||
|
||||
## 2.27.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
FROM node:20-alpine AS pruner
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
RUN yarn global add turbo@2.2.3
|
||||
COPY . .
|
||||
RUN turbo prune --scope="@nhost/dashboard" --docker
|
||||
|
||||
FROM node:20-alpine AS builder
|
||||
ARG TURBO_TOKEN
|
||||
ARG TURBO_TEAM
|
||||
|
||||
RUN apk add --no-cache libc6-compat python3 make g++
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV NEXT_PUBLIC_ENV=dev
|
||||
ENV NEXT_PUBLIC_NHOST_PLATFORM=false
|
||||
|
||||
# placeholders for URLs, will be replaced on runtime by entrypoint script
|
||||
ENV NEXT_PUBLIC_NHOST_ADMIN_SECRET=__NEXT_PUBLIC_NHOST_ADMIN_SECRET__
|
||||
ENV NEXT_PUBLIC_NHOST_AUTH_URL=__NEXT_PUBLIC_NHOST_AUTH_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_FUNCTIONS_URL=__NEXT_PUBLIC_NHOST_FUNCTIONS_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_GRAPHQL_URL=__NEXT_PUBLIC_NHOST_GRAPHQL_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_STORAGE_URL=__NEXT_PUBLIC_NHOST_STORAGE_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL=__NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL=__NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_API_URL=__NEXT_PUBLIC_NHOST_HASURA_API_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_CONFIGSERVER_URL=__NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__
|
||||
|
||||
RUN yarn global add pnpm@9.15.0
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-*.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
COPY config/ config/
|
||||
RUN pnpm build:dashboard
|
||||
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
USER nextjs
|
||||
|
||||
COPY --chown=nextjs:nodejs dashboard/docker-entrypoint.sh .
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/next.config.js .
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/package.json .
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/public ./dashboard/public
|
||||
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/.next/standalone/app ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/.next/static ./dashboard/.next/static
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
CMD ["node", "dashboard/server.js"]
|
||||
33
dashboard/Makefile
Normal file
33
dashboard/Makefile
Normal file
@@ -0,0 +1,33 @@
|
||||
ROOT_DIR?=$(abspath ../)
|
||||
include $(ROOT_DIR)/build/makefiles/general.makefile
|
||||
|
||||
TAG_NAME=@nhost/dashboard
|
||||
|
||||
|
||||
.PHONY: dev-env-cli-up
|
||||
dev-env-cli-up: dev-env-cli-build ## Starts the CLI using the dev dashboard
|
||||
./dev-env-cli.sh up
|
||||
|
||||
|
||||
.PHONY: dev-env-cli-up
|
||||
dev-env-cli-down: ## Stops the CLI using the dev dashboard
|
||||
./dev-env-cli.sh down
|
||||
|
||||
|
||||
.PHONY: dev-env-cli-build
|
||||
dev-env-cli-build: build-docker-image ## Build the docker image with the dev dashboard
|
||||
|
||||
|
||||
.PHONY: _dev-env-up
|
||||
_dev-env-up:
|
||||
@echo "Nothing to do"
|
||||
|
||||
|
||||
.PHONY: _dev-env-down
|
||||
_dev-env-down:
|
||||
@echo "Nothing to do"
|
||||
|
||||
|
||||
.PHONY: _dev-env-build
|
||||
_dev-env-build:
|
||||
@echo "Nothing to do"
|
||||
41
dashboard/dev-env-cli.sh
Executable file
41
dashboard/dev-env-cli.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export NHOST_DASHBOARD_VERSION=dashboard:0.0.0-dev
|
||||
FOLDER="$TMPDIR/nhost-dashboard-e2e"
|
||||
|
||||
|
||||
up() {
|
||||
echo "➜ Starting local CLI using locally built dashboard with tag $NHOST_DASHBOARD_VERSION"
|
||||
rm -rf "$FOLDER"
|
||||
mkdir -p "$FOLDER"
|
||||
cd "$FOLDER"
|
||||
nhost init
|
||||
nhost up --down-on-error
|
||||
}
|
||||
|
||||
|
||||
down() {
|
||||
if [ -d "$FOLDER" ]; then
|
||||
echo "➜ Stopping nhost environment"
|
||||
cd "$FOLDER"
|
||||
nhost down --volumes
|
||||
else
|
||||
echo "➜ No nhost environment found to stop"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
case "${1:-}" in
|
||||
"up")
|
||||
up
|
||||
;;
|
||||
"down")
|
||||
down
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {up|down}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -11,16 +11,17 @@ NEXT_PUBLIC_NHOST_STORAGE_URL="${NEXT_PUBLIC_NHOST_STORAGE_URL:-http://localhost
|
||||
NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL="${NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL:-http://localhost:9695}"
|
||||
NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL="${NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL:-http://localhost:9693}"
|
||||
NEXT_PUBLIC_NHOST_HASURA_API_URL="${NEXT_PUBLIC_NHOST_HASURA_API_URL:-http://localhost:8080}"
|
||||
NEXT_PUBLIC_NHOST_CONFIGSERVER_URL="${NEXT_PUBLIC_NHOST_CONFIGSERVER_URL:-""}"
|
||||
|
||||
# replace placeholders
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_ADMIN_SECRET__~${NEXT_PUBLIC_NHOST_ADMIN_SECRET}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_AUTH_URL__~${NEXT_PUBLIC_NHOST_AUTH_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_FUNCTIONS_URL__~${NEXT_PUBLIC_NHOST_FUNCTIONS_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_GRAPHQL_URL__~${NEXT_PUBLIC_NHOST_GRAPHQL_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_STORAGE_URL__~${NEXT_PUBLIC_NHOST_STORAGE_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__~${NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_API_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__~${NEXT_PUBLIC_NHOST_CONFIGSERVER_URL}~g" {} +
|
||||
find /dashboard/ -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_ADMIN_SECRET__~${NEXT_PUBLIC_NHOST_ADMIN_SECRET}~g" {} +
|
||||
find /dashboard/ -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_AUTH_URL__~${NEXT_PUBLIC_NHOST_AUTH_URL}~g" {} +
|
||||
find /dashboard/ -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_FUNCTIONS_URL__~${NEXT_PUBLIC_NHOST_FUNCTIONS_URL}~g" {} +
|
||||
find /dashboard/ -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_GRAPHQL_URL__~${NEXT_PUBLIC_NHOST_GRAPHQL_URL}~g" {} +
|
||||
find /dashboard/ -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_STORAGE_URL__~${NEXT_PUBLIC_NHOST_STORAGE_URL}~g" {} +
|
||||
find /dashboard/ -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__~${NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL}~g" {} +
|
||||
find /dashboard/ -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL}~g" {} +
|
||||
find /dashboard/ -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_API_URL}~g" {} +
|
||||
find /dashboard/ -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__~${NEXT_PUBLIC_NHOST_CONFIGSERVER_URL}~g" {} +
|
||||
|
||||
exec "$@"
|
||||
|
||||
@@ -13,6 +13,9 @@ test('should be able to ban and unban a user', async ({
|
||||
const password = faker.internet.password();
|
||||
|
||||
await createUser({ page, email, password });
|
||||
await page.waitForSelector(
|
||||
'div:has-text("User has been created successfully.")',
|
||||
);
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: `View ${email}`, exact: true })
|
||||
|
||||
@@ -11,6 +11,9 @@ test('should create a user', async ({ authenticatedNhostPage: page }) => {
|
||||
const password = faker.internet.password();
|
||||
|
||||
await createUser({ page, email, password });
|
||||
await page.waitForSelector(
|
||||
'div:has-text("User has been created successfully.")',
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: `View ${email}`, exact: true }),
|
||||
|
||||
@@ -14,6 +14,9 @@ test('should be able to delete a user', async ({
|
||||
const password = faker.internet.password();
|
||||
|
||||
await createUser({ page, email, password });
|
||||
await page.waitForSelector(
|
||||
'div:has-text("User has been created successfully.")',
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: `View ${email}`, exact: true }),
|
||||
@@ -39,6 +42,7 @@ test('should be able to delete a user', async ({
|
||||
await expect(
|
||||
page.getByRole('button', { name: `View ${email}`, exact: true }),
|
||||
).not.toBeVisible();
|
||||
await expect(page.getByText('User deleted successfully.')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be able to delete a user from the details page', async ({
|
||||
@@ -49,6 +53,10 @@ test('should be able to delete a user from the details page', async ({
|
||||
|
||||
await createUser({ page, email, password });
|
||||
|
||||
await page.waitForSelector(
|
||||
'div:has-text("User has been created successfully.")',
|
||||
);
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: `View ${email}`, exact: true })
|
||||
.click();
|
||||
@@ -68,4 +76,5 @@ test('should be able to delete a user from the details page', async ({
|
||||
await expect(
|
||||
page.getByRole('button', { name: `View ${email}`, exact: true }),
|
||||
).not.toBeVisible();
|
||||
await expect(page.getByText('User deleted successfully.')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -13,6 +13,9 @@ test('should be able to edit user roles from the details page', async ({
|
||||
const password = faker.internet.password();
|
||||
|
||||
await createUser({ page, email, password });
|
||||
await page.waitForSelector(
|
||||
'div:has-text("User has been created successfully.")',
|
||||
);
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: `View ${email}`, exact: true })
|
||||
|
||||
@@ -14,6 +14,10 @@ test('should be able to verify the email of a user', async ({
|
||||
|
||||
await createUser({ page, email, password });
|
||||
|
||||
await page.waitForSelector(
|
||||
'div:has-text("User has been created successfully.")',
|
||||
);
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: `View ${email}`, exact: true })
|
||||
.click();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||
import { expect, test } from '@/e2e/fixtures/auth-hook';
|
||||
import { prepareTable } from '@/e2e/utils';
|
||||
import { prepareTable, toPascalCase } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { snakeCase } from 'snake-case';
|
||||
|
||||
@@ -16,12 +16,12 @@ test('should create a simple table', async ({
|
||||
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));
|
||||
const tableName = toPascalCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
primaryKeys: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text' },
|
||||
@@ -38,6 +38,7 @@ test('should create a simple table', async ({
|
||||
await expect(
|
||||
page.getByRole('link', { name: tableName, exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'id' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create a table with unique constraints', async ({
|
||||
@@ -51,7 +52,7 @@ test('should create a table with unique constraints', async ({
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
primaryKeys: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text', unique: true },
|
||||
@@ -82,7 +83,7 @@ test('should create a table with nullable columns', async ({
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
primaryKeys: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text', nullable: true },
|
||||
@@ -113,7 +114,7 @@ test('should create a table with an identity column', async ({
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
primaryKeys: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'int4' },
|
||||
{ name: 'title', type: 'text', nullable: true },
|
||||
@@ -148,7 +149,7 @@ test('should create table with foreign key constraint', async ({
|
||||
await prepareTable({
|
||||
page,
|
||||
name: firstTableName,
|
||||
primaryKey: 'id',
|
||||
primaryKeys: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'name', type: 'text' },
|
||||
@@ -170,7 +171,7 @@ test('should create table with foreign key constraint', async ({
|
||||
await prepareTable({
|
||||
page,
|
||||
name: secondTableName,
|
||||
primaryKey: 'id',
|
||||
primaryKeys: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text' },
|
||||
@@ -196,6 +197,10 @@ test('should create table with foreign key constraint', async ({
|
||||
|
||||
await page.getByRole('button', { name: /add/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByTestId('foreignKeyFormSubmitButton'),
|
||||
).not.toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByText(`public.${firstTableName}.id`, { exact: true }),
|
||||
).toBeVisible();
|
||||
@@ -223,7 +228,7 @@ test('should not be able to create a table with a name that already exists', asy
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
primaryKeys: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'name', type: 'text' },
|
||||
@@ -243,7 +248,7 @@ test('should not be able to create a table with a name that already exists', asy
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
primaryKeys: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text' },
|
||||
@@ -258,3 +263,33 @@ 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();
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ test('should delete a table', async ({ authenticatedNhostPage: page }) => {
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
primaryKeys: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text' },
|
||||
@@ -59,7 +59,7 @@ test('should not be able to delete a table if other tables have foreign keys ref
|
||||
await prepareTable({
|
||||
page,
|
||||
name: firstTableName,
|
||||
primaryKey: 'id',
|
||||
primaryKeys: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'name', type: 'text' },
|
||||
@@ -81,7 +81,7 @@ test('should not be able to delete a table if other tables have foreign keys ref
|
||||
await prepareTable({
|
||||
page,
|
||||
name: secondTableName,
|
||||
primaryKey: 'id',
|
||||
primaryKeys: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text' },
|
||||
|
||||
@@ -21,7 +21,7 @@ test('should create a table with role permissions to select row', async ({
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
primaryKeys: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text' },
|
||||
@@ -69,7 +69,7 @@ test('should create a table with role permissions and a custom check to select r
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey: 'id',
|
||||
primaryKeys: ['id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'title', type: 'text' },
|
||||
|
||||
@@ -1,42 +1,46 @@
|
||||
/**
|
||||
* URL of the dashboard to test against.
|
||||
*/
|
||||
export const TEST_DASHBOARD_URL = process.env.NHOST_TEST_DASHBOARD_URL;
|
||||
export const TEST_DASHBOARD_URL = process.env.NHOST_TEST_DASHBOARD_URL!;
|
||||
|
||||
/**
|
||||
* Name of the organization to test against.
|
||||
*/
|
||||
export const TEST_ORGANIZATION_NAME = process.env.NHOST_TEST_ORGANIZATION_NAME;
|
||||
export const TEST_ORGANIZATION_NAME = process.env.NHOST_TEST_ORGANIZATION_NAME!;
|
||||
|
||||
/**
|
||||
* Slug of the organization to test against.
|
||||
*/
|
||||
export const TEST_ORGANIZATION_SLUG = process.env.NHOST_TEST_ORGANIZATION_SLUG;
|
||||
export const TEST_ORGANIZATION_SLUG = process.env.NHOST_TEST_ORGANIZATION_SLUG!;
|
||||
|
||||
/**
|
||||
* Name of the project to test against.
|
||||
*/
|
||||
export const TEST_PROJECT_NAME = process.env.NHOST_TEST_PROJECT_NAME;
|
||||
export const TEST_PROJECT_NAME = process.env.NHOST_TEST_PROJECT_NAME!;
|
||||
|
||||
/**
|
||||
* Subdomain of the project to test against.
|
||||
*/
|
||||
export const TEST_PROJECT_SUBDOMAIN = process.env.NHOST_TEST_PROJECT_SUBDOMAIN;
|
||||
export const TEST_PROJECT_SUBDOMAIN = process.env.NHOST_TEST_PROJECT_SUBDOMAIN!;
|
||||
|
||||
/**
|
||||
* Hasura admin secret of the test project to use.
|
||||
*/
|
||||
export const TEST_PROJECT_ADMIN_SECRET =
|
||||
process.env.NHOST_TEST_PROJECT_ADMIN_SECRET;
|
||||
process.env.NHOST_TEST_PROJECT_ADMIN_SECRET!;
|
||||
|
||||
/**
|
||||
* Email of the test account to use.
|
||||
*/
|
||||
export const TEST_USER_EMAIL = process.env.NHOST_TEST_USER_EMAIL;
|
||||
export const TEST_USER_EMAIL = process.env.NHOST_TEST_USER_EMAIL!;
|
||||
|
||||
/**
|
||||
* Password of the test account to use.
|
||||
*/
|
||||
export const TEST_USER_PASSWORD = process.env.NHOST_TEST_USER_PASSWORD;
|
||||
export const TEST_USER_PASSWORD = process.env.NHOST_TEST_USER_PASSWORD!;
|
||||
|
||||
export const TEST_PERSONAL_ORG_SLUG = process.env.NHOST_TEST_PERSONAL_ORG_SLUG;
|
||||
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);
|
||||
|
||||
@@ -10,10 +10,11 @@ export const test = base.extend<{ authenticatedNhostPage: Page }>({
|
||||
await page.goto('/');
|
||||
await page.waitForURL(
|
||||
`${TEST_DASHBOARD_URL}/orgs/${TEST_PERSONAL_ORG_SLUG}/projects`,
|
||||
{ waitUntil: 'networkidle' },
|
||||
{ waitUntil: 'load' },
|
||||
);
|
||||
await use(page);
|
||||
// update the context to get the new refresh token
|
||||
await page.waitForLoadState('load');
|
||||
await page.context().storageState({ path: AUTH_CONTEXT });
|
||||
await page.close();
|
||||
},
|
||||
|
||||
223
dashboard/e2e/onboarding/onboarding.test.ts
Normal file
223
dashboard/e2e/onboarding/onboarding.test.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { expect, test } from '@/e2e/fixtures/auth-hook';
|
||||
import {
|
||||
getCardExpiration,
|
||||
getOrgSlugFromUrl,
|
||||
getProjectSlugFromUrl,
|
||||
gotoUrl,
|
||||
loginWithFreeUser,
|
||||
setFreeUserStarterOrgSlug,
|
||||
setNewProjectName,
|
||||
setNewProjectSlug,
|
||||
} from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
let page: Page;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
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();
|
||||
const organizationName = faker.lorem.words(3).slice(0, 32);
|
||||
|
||||
await page.getByLabel('Organization Name').fill(organizationName);
|
||||
await page.getByText('Select organization type', { exact: true }).click();
|
||||
await page.getByText('Personal Project').nth(1).click();
|
||||
|
||||
await page.getByText('Pro', { exact: true }).click();
|
||||
|
||||
await page.getByText('Create Organization').click();
|
||||
|
||||
const stripeFrame = page
|
||||
.frameLocator('iframe[name="embedded-checkout"]')
|
||||
.first();
|
||||
stripeFrame.getByText('Subscribe to Nhost');
|
||||
await stripeFrame.getByLabel('Email').fill(faker.internet.email());
|
||||
|
||||
await stripeFrame
|
||||
.getByPlaceholder('1234 1234 1234 1234')
|
||||
.fill('4242424242424242');
|
||||
|
||||
await stripeFrame.getByPlaceholder('MM / YY').fill(getCardExpiration());
|
||||
await stripeFrame.getByPlaceholder('CVC').fill('123');
|
||||
await stripeFrame
|
||||
.getByPlaceholder('Full name on card')
|
||||
.fill('EndyTo EndyTest');
|
||||
await stripeFrame.locator('#billingCountry').scrollIntoViewIfNeeded();
|
||||
// Need to comment out for testing outside US START
|
||||
// await stripeFrame.getByPlaceholder('Address', { exact: true }).click();
|
||||
// stripeFrame.locator('span:has-text("Enter address manually")');
|
||||
// await stripeFrame.getByText('Enter address manually').click();
|
||||
// await stripeFrame
|
||||
// .getByPlaceholder('Address line 1', { exact: true })
|
||||
// .fill('123 Main Street');
|
||||
// await stripeFrame
|
||||
// .getByPlaceholder('City', { exact: true })
|
||||
// .fill('Springfield');
|
||||
// await stripeFrame.getByPlaceholder('ZIP', { exact: true }).fill('62701');
|
||||
// await stripeFrame.locator('#enableStripePass').click({ force: true });
|
||||
// Need to comment out for testing outside US END
|
||||
stripeFrame
|
||||
.getByTestId('hosted-payment-submit-button')
|
||||
.scrollIntoViewIfNeeded();
|
||||
await stripeFrame
|
||||
.getByTestId('hosted-payment-submit-button')
|
||||
.click({ force: true });
|
||||
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
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 () => {
|
||||
const newOrgSlug = getOrgSlugFromUrl(page.url());
|
||||
await gotoUrl(page, `/orgs/${newOrgSlug}/projects`);
|
||||
await page.getByRole('link', { name: 'Settings' }).click();
|
||||
|
||||
await page.waitForSelector('h3:has-text("Delete Organization")');
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await page.waitForSelector('h2:has-text("Delete Organization")');
|
||||
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
|
||||
await page.getByLabel("I'm sure I want to delete this Organization").click();
|
||||
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
await page.getByLabel('I understand this action cannot be undone').click();
|
||||
expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
|
||||
|
||||
await page.getByTestId('deleteOrgButton').click();
|
||||
|
||||
await page.waitForSelector('div:has-text("Deleting the organization")');
|
||||
await page.waitForSelector(
|
||||
'div:has-text("Successfully deleted the organization")',
|
||||
);
|
||||
|
||||
await page.waitForSelector('h2:has-text("Welcome to Nhost!")');
|
||||
});
|
||||
|
||||
test('should be able to upgrade an organization', async () => {
|
||||
await gotoUrl(page, `/onboarding`);
|
||||
expect(page.getByText('Welcome to Nhost!')).toBeVisible();
|
||||
const organizationName = faker.lorem.words(3).slice(0, 32);
|
||||
|
||||
await page.getByLabel('Organization Name').fill(organizationName);
|
||||
await page.getByText('Select organization type', { exact: true }).click();
|
||||
await page.getByText('Personal Project').nth(1).click();
|
||||
|
||||
await page.getByText('Create Organization').click();
|
||||
|
||||
await page.waitForSelector(
|
||||
'div:has-text("Organization created successfully!")',
|
||||
);
|
||||
await page.getByText('Select organization', { exact: true }).click();
|
||||
await page.getByLabel('Organizations').getByText(organizationName).click();
|
||||
|
||||
await page.waitForSelector('h2:has-text("Welcome to Nhost!")');
|
||||
await page.getByRole('link', { name: 'Billing' }).click();
|
||||
|
||||
await page.waitForSelector('h4:has-text("Subscription plan")');
|
||||
expect(page.getByText('Upgrade')).toBeEnabled();
|
||||
await page.getByText('Upgrade').click();
|
||||
await page.waitForSelector('h2:has-text("Upgrade Organization")');
|
||||
|
||||
await page.getByText('Pro', { exact: true }).click();
|
||||
|
||||
await page.getByTestId('upgradeOrgSubmitButton').click();
|
||||
await page.waitForSelector('button[data-testid="upgradeOrgSubmitButton"]', {
|
||||
state: 'hidden',
|
||||
});
|
||||
|
||||
const stripeFrame = page
|
||||
.frameLocator('iframe[name="embedded-checkout"]')
|
||||
.first();
|
||||
stripeFrame
|
||||
.locator('div[data-testid="product-summary"]')
|
||||
.waitFor({ state: 'visible' });
|
||||
await stripeFrame.getByLabel('Email').fill(faker.internet.email());
|
||||
|
||||
await stripeFrame
|
||||
.getByPlaceholder('1234 1234 1234 1234')
|
||||
.fill('4242424242424242');
|
||||
|
||||
await stripeFrame.getByPlaceholder('MM / YY').fill(getCardExpiration());
|
||||
await stripeFrame.getByPlaceholder('CVC').fill('123');
|
||||
await stripeFrame
|
||||
.getByPlaceholder('Full name on card')
|
||||
.fill('EndyTo EndyTest');
|
||||
await stripeFrame.locator('#billingCountry').scrollIntoViewIfNeeded();
|
||||
// Need to comment out for testing outside US START
|
||||
// await stripeFrame.getByPlaceholder('Address', { exact: true }).click();
|
||||
// stripeFrame.locator('span:has-text("Enter address manually")');
|
||||
// await stripeFrame.getByText('Enter address manually').click();
|
||||
// await stripeFrame
|
||||
// .getByPlaceholder('Address line 1', { exact: true })
|
||||
// .fill('123 Main Street');
|
||||
// await stripeFrame
|
||||
// .getByPlaceholder('City', { exact: true })
|
||||
// .fill('Springfield');
|
||||
// await stripeFrame.getByPlaceholder('ZIP', { exact: true }).fill('62701');
|
||||
// await stripeFrame.locator('#enableStripePass').click({ force: true });
|
||||
// Need to comment out for testing outside US END
|
||||
stripeFrame
|
||||
.getByTestId('hosted-payment-submit-button')
|
||||
.scrollIntoViewIfNeeded();
|
||||
await stripeFrame
|
||||
.getByTestId('hosted-payment-submit-button')
|
||||
.click({ force: true });
|
||||
await page.waitForSelector('div:has-text("Upgrading organization")');
|
||||
await page.waitForSelector(
|
||||
'div:has-text("Organization has been upgraded successfully.")',
|
||||
);
|
||||
await page.waitForSelector('span:has-text("Spending Notifications")');
|
||||
|
||||
await page.getByRole('link', { name: 'Settings' }).click();
|
||||
|
||||
await page.waitForSelector('h3:has-text("Delete Organization")');
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await page.waitForSelector('h2:has-text("Delete Organization")');
|
||||
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
|
||||
await page.getByLabel("I'm sure I want to delete this Organization").click();
|
||||
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
await page.getByLabel('I understand this action cannot be undone').click();
|
||||
expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
|
||||
|
||||
await page.getByTestId('deleteOrgButton').click();
|
||||
|
||||
await page.waitForSelector('div:has-text("Deleting the organization")');
|
||||
await page.waitForSelector(
|
||||
'div:has-text("Successfully deleted the organization")',
|
||||
);
|
||||
|
||||
await page.waitForSelector('h2:has-text("Welcome to Nhost!")');
|
||||
});
|
||||
@@ -18,7 +18,7 @@ setup('authenticate user', async ({ page }) => {
|
||||
|
||||
await page.waitForURL(
|
||||
`${TEST_DASHBOARD_URL}/orgs/${TEST_PERSONAL_ORG_SLUG}/projects`,
|
||||
{ waitUntil: 'networkidle' },
|
||||
{ waitUntil: 'load' },
|
||||
);
|
||||
await page.context().storageState({ path: 'e2e/.auth/user.json' });
|
||||
});
|
||||
|
||||
55
dashboard/e2e/setup/refresh-metadata.setup.ts
Normal file
55
dashboard/e2e/setup/refresh-metadata.setup.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/* eslint-disable no-console */
|
||||
import { TEST_PROJECT_ADMIN_SECRET, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||
import { test as setup } from '@playwright/test';
|
||||
|
||||
setup('refresh metadata', async () => {
|
||||
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({
|
||||
args: [
|
||||
{
|
||||
type: 'reload_metadata',
|
||||
args: {
|
||||
reload_sources: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
args: {},
|
||||
type: 'get_inconsistent_metadata',
|
||||
},
|
||||
],
|
||||
source: 'default',
|
||||
type: 'bulk',
|
||||
}),
|
||||
},
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
const message = `[${body.code}]:${body.error}`;
|
||||
throw new Error(message);
|
||||
} else {
|
||||
const isConsistent = body[0].is_consistent;
|
||||
if (isConsistent) {
|
||||
console.log('Metadata is consistent.');
|
||||
} else {
|
||||
console.error('Metadata is not consistent.');
|
||||
console.error(body[0].inconsistent_objects);
|
||||
throw new Error('Metadata is not consistent');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Log safe error information
|
||||
console.error(
|
||||
'Failed to refresh metadata:',
|
||||
error instanceof Error ? error.message : 'Unknown error',
|
||||
);
|
||||
throw new Error('Failed to refresh metadata');
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,12 @@
|
||||
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
|
||||
import {
|
||||
TEST_FREE_USER_EMAILS,
|
||||
TEST_ORGANIZATION_SLUG,
|
||||
TEST_PROJECT_SUBDOMAIN,
|
||||
TEST_USER_PASSWORD,
|
||||
} from '@/e2e/env';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { type Page } from '@playwright/test';
|
||||
import { add, format } from 'date-fns-v4';
|
||||
|
||||
/**
|
||||
* Open a project by navigating to the project's overview page.
|
||||
@@ -22,7 +28,7 @@ export async function navigateToProject({
|
||||
const projectUrl = `/orgs/${orgSlug}/projects/${projectSubdomain}`;
|
||||
|
||||
try {
|
||||
await page.goto(projectUrl, { waitUntil: 'networkidle' });
|
||||
await page.goto(projectUrl, { waitUntil: 'load' });
|
||||
await page.waitForURL(projectUrl, { timeout: 10000 });
|
||||
} catch (error) {
|
||||
console.error(`Failed to navigate to project URL: ${projectUrl}`, error);
|
||||
@@ -40,12 +46,12 @@ export async function navigateToProject({
|
||||
export async function prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKey,
|
||||
primaryKeys,
|
||||
columns,
|
||||
}: {
|
||||
page: Page;
|
||||
name: string;
|
||||
primaryKey: string;
|
||||
primaryKeys: string[];
|
||||
columns: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
@@ -54,7 +60,7 @@ export async function prepareTable({
|
||||
defaultValue?: string;
|
||||
}>;
|
||||
}) {
|
||||
if (!columns.some(({ name }) => name === primaryKey)) {
|
||||
if (!columns.some(({ name }) => primaryKeys.includes(name))) {
|
||||
throw new Error('Primary key must be one of the columns.');
|
||||
}
|
||||
|
||||
@@ -118,11 +124,13 @@ export async function prepareTable({
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// select the first column as primary key
|
||||
// await page.getByRole('button', { name: /primary key/i }).click();
|
||||
await page.getByLabel('Primary Key').click();
|
||||
await page.getByRole('option', { name: primaryKey, exact: true }).click();
|
||||
await Promise.all(
|
||||
primaryKeys.map(async (primaryKey) => {
|
||||
await page.getByRole('option', { name: primaryKey, exact: true }).click();
|
||||
}),
|
||||
);
|
||||
await page.getByText('Create a New Table').click();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,8 +221,101 @@ export async function clickPermissionButton({
|
||||
.click();
|
||||
}
|
||||
|
||||
export async function gotoAuthURL(page) {
|
||||
export async function gotoAuthURL(page: Page) {
|
||||
const authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`;
|
||||
await page.goto(authUrl);
|
||||
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
|
||||
await page.waitForURL(authUrl, { waitUntil: 'load' });
|
||||
}
|
||||
|
||||
export async function gotoUrl(page: Page, url: string) {
|
||||
await page.goto(url);
|
||||
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;
|
||||
}
|
||||
|
||||
export function getCardExpiration() {
|
||||
const now = add(new Date(), { years: 3 });
|
||||
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('Password').fill(TEST_USER_PASSWORD);
|
||||
await page.getByRole('button', { name: /sign in/i }).click();
|
||||
|
||||
await page.waitForSelector('h2:has-text("Welcome to Nhost!")', {
|
||||
timeout: 20000,
|
||||
});
|
||||
}
|
||||
|
||||
export function toPascalCase(str: string, divider = ' ') {
|
||||
return str
|
||||
.split(divider)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join('');
|
||||
}
|
||||
|
||||
@@ -5,17 +5,17 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
const { version } = require('./package.json');
|
||||
|
||||
const cspHeader = `
|
||||
default-src 'self' *.nhost.run ws://*.nhost.run nhost.run ws://nhost.run;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' cdn.segment.com js.stripe.com;
|
||||
connect-src 'self' *.nhost.run ws://*.nhost.run nhost.run ws://nhost.run discord.com api.segment.io api.segment.com cdn.segment.com nhost.zendesk.com;
|
||||
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: avatars.githubusercontent.com s.gravatar.com *.nhost.run nhost.run;
|
||||
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;
|
||||
frame-src 'self' js.stripe.com challenges.cloudflare.com;
|
||||
block-all-mixed-content;
|
||||
upgrade-insecure-requests;
|
||||
`;
|
||||
@@ -25,7 +25,7 @@ module.exports = withBundleAnalyzer({
|
||||
swcMinify: false,
|
||||
output: 'standalone',
|
||||
experimental: {
|
||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||
outputFileTracingRoot: path.join(__dirname, '../'),
|
||||
},
|
||||
publicRuntimeConfig: {
|
||||
version,
|
||||
@@ -38,10 +38,10 @@ module.exports = withBundleAnalyzer({
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
// {
|
||||
// key: 'Content-Security-Policy',
|
||||
// hgvalue: cspHeader.replace(/\s+/g, ' ').trim(),
|
||||
// },
|
||||
{
|
||||
key: 'Content-Security-Policy',
|
||||
value: cspHeader.replace(/\s+/g, ' ').trim(),
|
||||
},
|
||||
{
|
||||
key: 'X-Frame-Options',
|
||||
value: 'DENY',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "2.27.0",
|
||||
"version": "0.0.0-dev",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -9,15 +9,17 @@
|
||||
"analyze": "ANALYZE=true pnpm build --no-lint",
|
||||
"start": "next start",
|
||||
"lint": "next lint --max-warnings 0",
|
||||
"test": "vitest",
|
||||
"test": "vitest --run",
|
||||
"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",
|
||||
"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": "pnpm install-browsers && pnpm playwright test --config=playwright.config.ts",
|
||||
"e2e-local": "pnpm install-browsers && pnpm playwright test --config=playwright.local.config.ts"
|
||||
"e2e:tests": "pnpm playwright test --config=playwright.config.ts -x",
|
||||
"e2e": "pnpm e2e:tests --project=main",
|
||||
"e2e:local": "pnpm e2e:tests --project=local",
|
||||
"e2e:onboarding": "pnpm e2e:tests --project=onboarding"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.9",
|
||||
@@ -37,29 +39,30 @@
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@icons-pack/react-simple-icons": "^9.6.0",
|
||||
"@marsidev/react-turnstile": "^1.0.2",
|
||||
"@mui/base": "5.0.0-beta.31",
|
||||
"@mui/material": "^5.15.14",
|
||||
"@mui/system": "^5.15.14",
|
||||
"@mui/x-date-pickers": "^5.0.20",
|
||||
"@nhost/nextjs": "workspace:*",
|
||||
"@nhost/react-apollo": "workspace:*",
|
||||
"@nhost/nhost-js": "workspace:^",
|
||||
"@radix-ui/react-accordion": "^1.2.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.3",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-hover-card": "^1.1.2",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.3",
|
||||
"@radix-ui/react-progress": "^1.1.0",
|
||||
"@radix-ui/react-radio-group": "^1.2.0",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@segment/analytics-next": "^1.77.0",
|
||||
"@simplewebauthn/browser": "^9.0.1",
|
||||
"@stripe/react-stripe-js": "^2.6.2",
|
||||
"@stripe/stripe-js": "^1.54.2",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
@@ -85,9 +88,10 @@
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.16.0",
|
||||
"just-kebab-case": "^4.2.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lucide-react": "^0.416.0",
|
||||
"next": "^14.2.25",
|
||||
"next": "^14.2.31",
|
||||
"next-nprogress-bar": "^2.3.13",
|
||||
"next-seo": "^6.5.0",
|
||||
"next-themes": "^0.3.0",
|
||||
@@ -95,7 +99,7 @@
|
||||
"pluralize": "^8.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
"react-complex-tree": "^2.4.5",
|
||||
"react-complex-tree": "^2.6.0",
|
||||
"react-day-picker": "9.6.3",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
@@ -105,7 +109,7 @@
|
||||
"react-is": "18.2.0",
|
||||
"react-loading-skeleton": "^2.2.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-merge-refs": "^1.1.0",
|
||||
"react-merge-refs": "^3.0.2",
|
||||
"react-resizable-layout": "^0.7.2",
|
||||
"react-table": "^7.8.0",
|
||||
"recoil": "^0.7.7",
|
||||
@@ -126,13 +130,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.3",
|
||||
"@eslint/js": "9.26.0",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
"@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.47.0",
|
||||
"@playwright/test": "1.54.1",
|
||||
"@storybook/addon-actions": "^6.5.16",
|
||||
"@storybook/addon-essentials": "^6.5.16",
|
||||
"@storybook/addon-interactions": "^6.5.16",
|
||||
@@ -153,7 +158,7 @@
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/node": "^16.18.93",
|
||||
"@types/pluralize": "^0.0.30",
|
||||
"@types/react": "^18.2.73",
|
||||
"@types/react": "18.2.73",
|
||||
"@types/react-dom": "^18.2.23",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/testing-library__jest-dom": "^5.14.9",
|
||||
@@ -163,6 +168,7 @@
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@vitest/coverage-v8": "^0.32.4",
|
||||
"audit-ci": "^6.6.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"babel-loader": "^8.3.0",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
@@ -174,10 +180,15 @@
|
||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||
"eslint-config-next": "^13.5.6",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-flowtype": "^8.0.3",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.2.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"lint-staged": "^15.2.2",
|
||||
"msw": "^1.3.5",
|
||||
@@ -194,7 +205,7 @@
|
||||
"tailwindcss": "^3.4.12",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
||||
"vite": "^5.4.17",
|
||||
"vite": "^5.4.20",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^0.32.4"
|
||||
},
|
||||
@@ -212,5 +223,21 @@
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
},
|
||||
"pnpm": {
|
||||
"packageExtensions": {
|
||||
"@uiw/codemirror-theme-bbedit": {
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@uiw/codemirror-theme-github": {
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ dotenv.config({ path: path.resolve(__dirname, '.env.test') });
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
timeout: 60 * 1000,
|
||||
timeout: 120 * 1000,
|
||||
expect: {
|
||||
timeout: 10000,
|
||||
},
|
||||
@@ -17,7 +17,7 @@ export default defineConfig({
|
||||
reporter: 'html',
|
||||
use: {
|
||||
actionTimeout: 0,
|
||||
trace: 'on-first-retry',
|
||||
trace: 'retain-on-failure',
|
||||
baseURL: process.env.NHOST_TEST_DASHBOARD_URL,
|
||||
launchOptions: {
|
||||
slowMo: 500,
|
||||
@@ -34,13 +34,28 @@ export default defineConfig({
|
||||
testMatch: ['**/teardown/*.teardown.ts'],
|
||||
},
|
||||
{
|
||||
name: 'chromium',
|
||||
name: 'main',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
storageState: 'e2e/.auth/user.json',
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
grepInvert: [/Local Dashboard CLI e2e tests/],
|
||||
testIgnore: ['onboarding.test.ts', 'cli-local-dashboard.test.ts'],
|
||||
},
|
||||
{
|
||||
name: 'local',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
baseURL: '', // Local dashboard URL
|
||||
},
|
||||
testMatch: 'cli-local-dashboard.test.ts',
|
||||
},
|
||||
{
|
||||
name: 'onboarding',
|
||||
testMatch: 'onboarding.test.ts',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
actionTimeout: 0,
|
||||
trace: 'on-first-retry',
|
||||
baseURL: '', // Local dashboard URL
|
||||
launchOptions: {
|
||||
slowMo: 500,
|
||||
},
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
testMatch: ['**/e2e/cli-local-dashboard/**'],
|
||||
},
|
||||
],
|
||||
});
|
||||
23679
dashboard/pnpm-lock.yaml
generated
Normal file
23679
dashboard/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
217
dashboard/project.nix
Normal file
217
dashboard/project.nix
Normal file
@@ -0,0 +1,217 @@
|
||||
{ self, pkgs, nix2containerPkgs, nix-filter, nixops-lib }:
|
||||
let
|
||||
name = "dashboard";
|
||||
version = "0.0.0-dev";
|
||||
created = "1970-01-01T00:00:00Z";
|
||||
submodule = "${name}";
|
||||
|
||||
node_modules = nixops-lib.js.mkNodeModules {
|
||||
name = "node-modules-${name}";
|
||||
version = "0.0.0-dev";
|
||||
|
||||
src = nix-filter.lib.filter {
|
||||
root = ./..;
|
||||
include = [
|
||||
".npmrc"
|
||||
"package.json"
|
||||
"pnpm-workspace.yaml"
|
||||
"pnpm-lock.yaml"
|
||||
"${submodule}/package.json"
|
||||
"${submodule}/pnpm-lock.yaml"
|
||||
];
|
||||
};
|
||||
|
||||
pnpmOpts = "--filter . --filter './${submodule}/**'";
|
||||
|
||||
preBuild = ''
|
||||
mkdir packages
|
||||
cp -r ${self.packages.${pkgs.system}.nhost-js} packages/nhost-js
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
src = nix-filter.lib.filter {
|
||||
root = ../.;
|
||||
include = with nix-filter.lib; [
|
||||
isDirectory
|
||||
".npmrc"
|
||||
".prettierignore"
|
||||
".prettierrc.js"
|
||||
"audit-ci.jsonc"
|
||||
"package.json"
|
||||
"pnpm-workspace.yaml"
|
||||
"pnpm-lock.yaml"
|
||||
"turbo.json"
|
||||
"${submodule}/.env.test"
|
||||
"${submodule}/.env.example"
|
||||
"${submodule}/.eslintignore"
|
||||
"${submodule}/.eslintrc.js"
|
||||
"${submodule}/.gitignore"
|
||||
"${submodule}/.lintstagedrc.json"
|
||||
"${submodule}/.npmrc"
|
||||
"${submodule}/.prettierignore"
|
||||
"${submodule}/components.json"
|
||||
"${submodule}/graphite.graphql.config.yaml"
|
||||
"${submodule}/graphql.config.yaml"
|
||||
"${submodule}/next-env.d.ts"
|
||||
"${submodule}/next.config.js"
|
||||
"${submodule}/package.json"
|
||||
"${submodule}/pnpm-lock.yaml"
|
||||
"${submodule}/playwright.config.ts"
|
||||
"${submodule}/postcss.config.js"
|
||||
"${submodule}/prettier.config.js"
|
||||
"${submodule}/react-table-config.d.ts"
|
||||
"${submodule}/tailwind.config.js"
|
||||
"${submodule}/tsconfig.json"
|
||||
"${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")
|
||||
];
|
||||
};
|
||||
|
||||
checkDeps = with pkgs; [ nhost-cli ];
|
||||
|
||||
buildInputs = with pkgs; [ nodejs ];
|
||||
|
||||
nativeBuildInputs = with pkgs; [ pnpm cacert ];
|
||||
in
|
||||
rec {
|
||||
devShell = nixops-lib.js.devShell {
|
||||
inherit node_modules;
|
||||
|
||||
buildInputs = with pkgs;[
|
||||
nodePackages.vercel
|
||||
] ++ checkDeps ++ buildInputs ++ nativeBuildInputs;
|
||||
};
|
||||
|
||||
entrypoint = pkgs.writeScriptBin "docker-entrypoint.sh" (builtins.readFile ./docker-entrypoint.sh);
|
||||
|
||||
check = nixops-lib.js.check {
|
||||
inherit src node_modules submodule buildInputs nativeBuildInputs checkDeps;
|
||||
|
||||
preCheck = ''
|
||||
rm -rf packages/nhost-js
|
||||
cp -r ${self.packages.${pkgs.system}.nhost-js} packages/nhost-js
|
||||
'';
|
||||
};
|
||||
|
||||
check-staging = pkgs.runCommand "check"
|
||||
{
|
||||
nativeBuildInputs = checkDeps ++ buildInputs ++ nativeBuildInputs;
|
||||
} ''
|
||||
cp -r ${src}/* .
|
||||
chmod +w -R .
|
||||
|
||||
cp -r ${node_modules}/node_modules/ node_modules
|
||||
cp -r ${node_modules}/dashboard/node_modules/ dashboard/node_modules
|
||||
|
||||
rm -rf packages/nhost-js
|
||||
cp -r ${self.packages.${pkgs.system}.nhost-js} packages/nhost-js
|
||||
|
||||
export HOME=$TMPDIR
|
||||
|
||||
cd dashboard
|
||||
|
||||
echo "➜ Running e2e tests"
|
||||
pnpm e2e
|
||||
|
||||
echo "➜ Running e2e tests (onboarding)"
|
||||
pnpm e2e:onboarding
|
||||
|
||||
echo "➜ Running e2e tests against local Nhost instance"
|
||||
pnpm e2e:local
|
||||
|
||||
mkdir -p $out
|
||||
'';
|
||||
|
||||
|
||||
package = pkgs.stdenv.mkDerivation {
|
||||
inherit name version src;
|
||||
|
||||
nativeBuildInputs = with pkgs; [ pnpm cacert nodejs ];
|
||||
buildInputs = with pkgs; [ nodejs ];
|
||||
|
||||
configurePhase = ''
|
||||
export NEXT_PUBLIC_NHOST_ADMIN_SECRET=__NEXT_PUBLIC_NHOST_ADMIN_SECRET__
|
||||
export NEXT_PUBLIC_NHOST_AUTH_URL=__NEXT_PUBLIC_NHOST_AUTH_URL__
|
||||
export NEXT_PUBLIC_NHOST_FUNCTIONS_URL=__NEXT_PUBLIC_NHOST_FUNCTIONS_URL__
|
||||
export NEXT_PUBLIC_NHOST_GRAPHQL_URL=__NEXT_PUBLIC_NHOST_GRAPHQL_URL__
|
||||
export NEXT_PUBLIC_NHOST_STORAGE_URL=__NEXT_PUBLIC_NHOST_STORAGE_URL__
|
||||
export NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL=__NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__
|
||||
export NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL=__NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__
|
||||
export NEXT_PUBLIC_NHOST_HASURA_API_URL=__NEXT_PUBLIC_NHOST_HASURA_API_URL__
|
||||
export NEXT_PUBLIC_NHOST_CONFIGSERVER_URL=__NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__
|
||||
'';
|
||||
|
||||
buildPhase = ''
|
||||
cp -r ${node_modules}/node_modules/ node_modules
|
||||
cp -r ${node_modules}/dashboard/node_modules/ dashboard/node_modules
|
||||
|
||||
rm -rf packages/nhost-js
|
||||
cp -r ${self.packages.${pkgs.system}.nhost-js} packages/nhost-js
|
||||
|
||||
cd dashboard
|
||||
pnpm build
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
cp -r .next/standalone $out
|
||||
|
||||
mkdir -p $out/dashboard/.next
|
||||
cp -r .next/static $out/dashboard/.next/static
|
||||
cp -r public $out/dashboard/public
|
||||
'';
|
||||
};
|
||||
|
||||
dockerImage = pkgs.runCommand "image-as-dir" { } ''
|
||||
${(nix2containerPkgs.nix2container.buildImage {
|
||||
inherit name created;
|
||||
tag = version;
|
||||
maxLayers = 100;
|
||||
|
||||
copyToRoot = pkgs.buildEnv {
|
||||
name = "image";
|
||||
paths = [
|
||||
package
|
||||
(pkgs.writeTextFile {
|
||||
name = "tmp-file";
|
||||
text = ''
|
||||
dummy file to generate tmpdir
|
||||
'';
|
||||
destination = "/tmp/tmp-file";
|
||||
})
|
||||
pkgs.busybox
|
||||
];
|
||||
};
|
||||
|
||||
config = {
|
||||
Env = [
|
||||
"TMPDIR=/tmp"
|
||||
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
|
||||
"NEXT_TELEMETRY_DISABLED=1"
|
||||
"NEXT_PUBLIC_ENV=dev"
|
||||
"NEXT_PUBLIC_NHOST_PLATFORM=false"
|
||||
# placeholders for URLs, will be replaced on runtime by entrypoint script
|
||||
"NEXT_PUBLIC_NHOST_ADMIN_SECRET=__NEXT_PUBLIC_NHOST_ADMIN_SECRET__"
|
||||
"NEXT_PUBLIC_NHOST_AUTH_URL=__NEXT_PUBLIC_NHOST_AUTH_URL__"
|
||||
"NEXT_PUBLIC_NHOST_FUNCTIONS_URL=__NEXT_PUBLIC_NHOST_FUNCTIONS_URL__"
|
||||
"NEXT_PUBLIC_NHOST_GRAPHQL_URL=__NEXT_PUBLIC_NHOST_GRAPHQL_URL__"
|
||||
"NEXT_PUBLIC_NHOST_STORAGE_URL=__NEXT_PUBLIC_NHOST_STORAGE_URL__"
|
||||
"NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL=__NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__"
|
||||
"NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL=__NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__"
|
||||
"NEXT_PUBLIC_NHOST_HASURA_API_URL=__NEXT_PUBLIC_NHOST_HASURA_API_URL__"
|
||||
"NEXT_PUBLIC_NHOST_CONFIGSERVER_URL=__NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__"
|
||||
];
|
||||
Entrypoint = [
|
||||
"${entrypoint}/bin/docker-entrypoint.sh" "${pkgs.nodejs}/bin/node" "/dashboard/server.js"
|
||||
];
|
||||
};
|
||||
}).copyTo}/bin/copy-to dir:$out
|
||||
'';
|
||||
}
|
||||
|
||||
|
||||
12
dashboard/public/assets/brands/entraid.svg
Normal file
12
dashboard/public/assets/brands/entraid.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 16 KiB |
12
dashboard/public/assets/brands/light/azuread.svg
Normal file
12
dashboard/public/assets/brands/light/azuread.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 16 KiB |
12
dashboard/public/assets/brands/light/entraid.svg
Normal file
12
dashboard/public/assets/brands/light/entraid.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 16 KiB |
59
dashboard/src/components/auth/SignInRightColumn.tsx
Normal file
59
dashboard/src/components/auth/SignInRightColumn.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
export function SignInRightColumn() {
|
||||
return (
|
||||
<div className="grid gap-6 font-[Inter]">
|
||||
<div className="text-center">
|
||||
<h2 className="mb-2 text-2xl font-semibold text-white">
|
||||
Ship 10x faster
|
||||
</h2>
|
||||
<p className="text-sm text-[#A2B3BE]">
|
||||
Skip months of backend setup and focus on building what matters
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-white/10 bg-gradient-to-r from-[#0052CD]/10 to-[#FF02F5]/10 p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
<Image
|
||||
src="/assets/signup/CircleWavyCheck.svg"
|
||||
width={20}
|
||||
height={20}
|
||||
alt="Check"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-2 text-sm font-semibold text-white">
|
||||
From idea to production
|
||||
</h3>
|
||||
<p className="text-xs text-[#A2B3BE]">
|
||||
Everything you need to ship fast, without the setup complexity.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-white/10 bg-gradient-to-r from-[#0052CD]/10 to-[#FF02F5]/10 p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
<Image
|
||||
src="/assets/key.svg"
|
||||
width={20}
|
||||
height={20}
|
||||
alt="Security"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-2 text-sm font-semibold text-white">
|
||||
Sleep easy at night
|
||||
</h3>
|
||||
<p className="text-xs text-[#A2B3BE]">
|
||||
Rock-solid security so you can focus on building, not
|
||||
vulnerabilities.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { useSSRLocalStorage } from '@/hooks/useSSRLocalStorage';
|
||||
import { X } from 'lucide-react';
|
||||
import NextLink from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface CookieConsentProps {
|
||||
onAccept: () => void;
|
||||
}
|
||||
|
||||
export default function CookieConsent({ onAccept }: CookieConsentProps) {
|
||||
const [consentGiven, setConsentGiven] = useSSRLocalStorage<boolean | null>(
|
||||
'cookie-consent',
|
||||
null,
|
||||
);
|
||||
const [showBanner, setShowBanner] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (consentGiven === null) {
|
||||
setShowBanner(true);
|
||||
} else if (consentGiven === true) {
|
||||
onAccept();
|
||||
}
|
||||
}, [consentGiven, onAccept]);
|
||||
|
||||
const handleAccept = () => {
|
||||
setConsentGiven(true);
|
||||
setShowBanner(false);
|
||||
onAccept();
|
||||
};
|
||||
|
||||
const handleDecline = () => {
|
||||
setConsentGiven(false);
|
||||
setShowBanner(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
handleDecline();
|
||||
};
|
||||
|
||||
if (!showBanner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-50 w-96">
|
||||
<div className="rounded-lg border bg-black/95 p-6 shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="mb-3 text-sm font-semibold text-white">
|
||||
We use cookies for payments and analytics to improve our services.
|
||||
</h3>
|
||||
<p className="mb-4 text-xs text-[#A2B3BE]">
|
||||
<NextLink
|
||||
href="https://nhost.io/legal/privacy-policy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-white underline hover:no-underline"
|
||||
>
|
||||
Learn more
|
||||
</NextLink>
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleAccept}
|
||||
size="sm"
|
||||
className="bg-blue-600 px-3 py-1 text-xs text-white hover:bg-blue-700"
|
||||
>
|
||||
Accept all
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDecline}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-gray-600 px-3 py-1 text-xs text-white hover:bg-gray-800"
|
||||
>
|
||||
Essential only
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
type="button"
|
||||
className="text-[#A2B3BE] hover:text-white"
|
||||
aria-label="Close cookie banner"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
dashboard/src/components/common/CookieConsent/index.ts
Normal file
2
dashboard/src/components/common/CookieConsent/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as CookieConsent } from './CookieConsent';
|
||||
|
||||
@@ -64,29 +64,29 @@ describe('DateTimePicker', () => {
|
||||
await screen.findByRole('button', { name: 'Select' }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(await screen.getByText('March 2025')).toBeInTheDocument();
|
||||
expect(screen.getByText('March 2025')).toBeInTheDocument();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Go to the Next Month' }),
|
||||
);
|
||||
expect(screen.getByText('April 2025')).toBeInTheDocument();
|
||||
|
||||
await user.click(await screen.getByText('13'));
|
||||
await user.click(screen.getByText('13'));
|
||||
|
||||
const hoursInput = await screen.getByLabelText('Hours');
|
||||
const hoursInput = screen.getByLabelText('Hours');
|
||||
await user.type(hoursInput, '11');
|
||||
|
||||
const minutesInput = await screen.getByLabelText('Minutes');
|
||||
const minutesInput = screen.getByLabelText('Minutes');
|
||||
await user.type(minutesInput, '12');
|
||||
|
||||
const secondsInput = await screen.getByLabelText('Seconds');
|
||||
const secondsInput = screen.getByLabelText('Seconds');
|
||||
await user.type(secondsInput, '13');
|
||||
|
||||
user.click(await screen.getByRole('button', { name: 'Select' }));
|
||||
await user.click(screen.getByRole('button', { name: 'Select' }));
|
||||
|
||||
await waitFor(async () =>
|
||||
expect(
|
||||
await screen.queryByRole('button', { name: 'Select' }),
|
||||
screen.queryByRole('button', { name: 'Select' }),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
|
||||
@@ -119,7 +119,7 @@ describe('DateTimePicker', () => {
|
||||
await user.type(tzInput, 'America/Chicago{ArrowDown}{Enter}');
|
||||
|
||||
expect(
|
||||
await screen.queryByPlaceholderText('Search timezones...'),
|
||||
screen.queryByPlaceholderText('Search timezones...'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
expect(await screen.findByText(/Timezone: /i)).toHaveTextContent(
|
||||
@@ -148,7 +148,7 @@ describe('DateTimePicker', () => {
|
||||
'Timezone: UTC+02:00',
|
||||
);
|
||||
|
||||
expect(await screen.getByText('March 2025')).toBeInTheDocument();
|
||||
expect(screen.getByText('March 2025')).toBeInTheDocument();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Go to the Next Month' }),
|
||||
@@ -156,7 +156,7 @@ describe('DateTimePicker', () => {
|
||||
|
||||
expect(screen.getByText('April 2025')).toBeInTheDocument();
|
||||
|
||||
await user.click(await screen.getByText('18'));
|
||||
await user.click(screen.getByText('18'));
|
||||
|
||||
expect(await screen.findByText(/Timezone: /i)).toHaveTextContent(
|
||||
'Timezone: UTC+03:00',
|
||||
@@ -166,9 +166,9 @@ describe('DateTimePicker', () => {
|
||||
screen.getByRole('button', { name: 'Go to the Previous Month' }),
|
||||
);
|
||||
|
||||
expect(await screen.getByText('March 2025')).toBeInTheDocument();
|
||||
expect(screen.getByText('March 2025')).toBeInTheDocument();
|
||||
|
||||
await user.click(await screen.getByText('21'));
|
||||
await user.click(screen.getByText('21'));
|
||||
|
||||
expect(await screen.findByText(/Timezone: /i)).toHaveTextContent(
|
||||
'Timezone: UTC+02:00',
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface DialogContextProps {
|
||||
/**
|
||||
* Call this function to open an alert dialog.
|
||||
*/
|
||||
openAlertDialog: <TPayload = string>(config?: DialogConfig<TPayload>) => void;
|
||||
openAlertDialog: <TPayload = string>(config: DialogConfig<TPayload>) => void;
|
||||
/**
|
||||
* Call this function to close the active dialog.
|
||||
*/
|
||||
|
||||
@@ -23,6 +23,10 @@ import {
|
||||
drawerReducer,
|
||||
} from './dialogReducers';
|
||||
|
||||
function isBaseSyntheticEvent(event: any): event is BaseSyntheticEvent {
|
||||
return event?.type !== undefined;
|
||||
}
|
||||
|
||||
function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -87,7 +91,7 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
drawerDispatch({ type: 'CLEAR_DRAWER_CONTENT' });
|
||||
}, []);
|
||||
|
||||
function openAlertDialog<TConfig = string>(config?: DialogConfig<TConfig>) {
|
||||
function openAlertDialog<TConfig = string>(config: DialogConfig<TConfig>) {
|
||||
alertDialogDispatch({ type: 'OPEN_ALERT', payload: config });
|
||||
}
|
||||
|
||||
@@ -122,8 +126,12 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
);
|
||||
|
||||
const closeDrawerWithDirtyGuard = useCallback(
|
||||
(event?: BaseSyntheticEvent) => {
|
||||
if (isDrawerDirty.current && event?.type !== 'submit') {
|
||||
(event?: any) => {
|
||||
if (
|
||||
isDrawerDirty.current &&
|
||||
isBaseSyntheticEvent(event) &&
|
||||
event.type !== 'submit'
|
||||
) {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDrawer } });
|
||||
return;
|
||||
@@ -135,8 +143,12 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
);
|
||||
|
||||
const closeDialogWithDirtyGuard = useCallback(
|
||||
(event?: BaseSyntheticEvent) => {
|
||||
if (isDialogDirty.current && event?.type !== 'submit') {
|
||||
(event?: any) => {
|
||||
if (
|
||||
isDialogDirty.current &&
|
||||
isBaseSyntheticEvent(event) &&
|
||||
event.type !== 'submit'
|
||||
) {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDialog } });
|
||||
return;
|
||||
@@ -250,7 +262,7 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
<BaseDialog
|
||||
{...dialogProps}
|
||||
title={dialogTitle}
|
||||
open={dialogOpen}
|
||||
open={!!dialogOpen}
|
||||
onClose={closeDialogWithDirtyGuard}
|
||||
TransitionProps={{ onExited: clearDialogContent, unmountOnExit: false }}
|
||||
PaperProps={{
|
||||
|
||||
@@ -115,7 +115,7 @@ export function drawerReducer(
|
||||
}
|
||||
|
||||
export type AlertDialogAction =
|
||||
| { type: 'OPEN_ALERT'; payload?: DialogConfig }
|
||||
| { type: 'OPEN_ALERT'; payload: DialogConfig }
|
||||
| { type: 'HIDE_ALERT' }
|
||||
| { type: 'CLEAR_ALERT_CONTENT' };
|
||||
|
||||
|
||||
518
dashboard/src/components/common/MfaOtpForm/MfaOtpForm.test.tsx
Normal file
518
dashboard/src/components/common/MfaOtpForm/MfaOtpForm.test.tsx
Normal file
@@ -0,0 +1,518 @@
|
||||
import { render, screen, TestUserEvent, waitFor } from '@/tests/testUtils';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import MfaOtpForm from './MfaOtpForm';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
toastError: vi.fn(),
|
||||
}));
|
||||
// Mock react-hot-toast
|
||||
vi.mock('react-hot-toast', async () => {
|
||||
const actualToast = await vi.importActual<any>('react-hot-toast');
|
||||
return {
|
||||
...actualToast,
|
||||
default: {
|
||||
...actualToast.default,
|
||||
error: mocks.toastError,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the toast style props utility
|
||||
vi.mock('@/utils/constants/settings', () => ({
|
||||
getToastStyleProps: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
describe('MfaOtpForm', () => {
|
||||
const mockSendMfaOtp = vi.fn();
|
||||
const mockRequestNewMfaTicket = vi.fn();
|
||||
const user = new TestUserEvent();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
sendMfaOtp: mockSendMfaOtp,
|
||||
loading: false,
|
||||
requestNewMfaTicket: mockRequestNewMfaTicket,
|
||||
} as any;
|
||||
|
||||
describe('Rendering and Initial State', () => {
|
||||
it('renders with correct initial state', () => {
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue('');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it('focuses input on mount', () => {
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
expect(input).toHaveFocus();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Input Validation and Formatting', () => {
|
||||
it('only accepts numeric characters', async () => {
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
await user.type(input, 'abc123def456');
|
||||
|
||||
expect(input).toHaveValue('123456');
|
||||
});
|
||||
|
||||
it('filters out non-numeric characters in real time', async () => {
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
await user.type(input, '1a2b3c');
|
||||
|
||||
expect(input).toHaveValue('123');
|
||||
});
|
||||
|
||||
it('button is disabled when input has fewer than 6 digits', async () => {
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '12345');
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it('button is enabled when input has exactly 6 digits', async () => {
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '123456');
|
||||
expect(button).toBeEnabled();
|
||||
});
|
||||
|
||||
it('button is disabled when input has more than 6 digits', async () => {
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '6123457');
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading States', () => {
|
||||
it('disables input and button when loading prop is true', () => {
|
||||
render(<MfaOtpForm {...defaultProps} loading />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button');
|
||||
|
||||
expect(input).toBeDisabled();
|
||||
expect(button).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Verifying...' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('input and button are disabled during submission', async () => {
|
||||
// Mock sendMfaOtp to return a promise that we can control
|
||||
const promise = new Promise(() => {}); // Never resolves
|
||||
mockSendMfaOtp.mockReturnValue(promise);
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.click(button);
|
||||
|
||||
expect(input).toBeDisabled();
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Submission', () => {
|
||||
it('triggers sendMfaOtp with correct code on button click', async () => {
|
||||
mockSendMfaOtp.mockResolvedValue({ success: true });
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.click(button);
|
||||
|
||||
expect(mockSendMfaOtp).toHaveBeenCalledWith('123456');
|
||||
expect(mockSendMfaOtp).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not submit when input has fewer than 6 digits', async () => {
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '12345');
|
||||
await user.click(button);
|
||||
|
||||
expect(mockSendMfaOtp).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not submit multiple times when already submitting', async () => {
|
||||
let resolvePromise: (value: any) => void;
|
||||
const promise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
mockSendMfaOtp.mockReturnValue(promise);
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.click(button);
|
||||
await user.click(button); // Second click should be ignored
|
||||
|
||||
expect(mockSendMfaOtp).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Resolve the promise to clean up
|
||||
resolvePromise!({ success: true });
|
||||
await waitFor(async () => {
|
||||
await promise;
|
||||
});
|
||||
});
|
||||
|
||||
it('manages submission state properly', async () => {
|
||||
let resolvePromise: (value: any) => void;
|
||||
const promise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
mockSendMfaOtp.mockReturnValue(promise);
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.click(button);
|
||||
|
||||
// During submission
|
||||
expect(button).toBeDisabled();
|
||||
expect(input).toBeDisabled();
|
||||
|
||||
// Resolve the promise
|
||||
resolvePromise!({ success: true });
|
||||
await waitFor(async () => {
|
||||
await promise;
|
||||
});
|
||||
|
||||
// After submission
|
||||
await waitFor(() => {
|
||||
expect(button).not.toBeDisabled();
|
||||
expect(input).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('displays error toast when sendMfaOtp returns an error', async () => {
|
||||
const errorMessage = 'Invalid TOTP code';
|
||||
|
||||
mockSendMfaOtp.mockRejectedValueOnce({ message: errorMessage });
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.toastError).toHaveBeenCalledWith(errorMessage, {});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows generic error message when no specific error message is provided', async () => {
|
||||
mockSendMfaOtp.mockRejectedValueOnce({});
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.toastError).toHaveBeenCalledWith(
|
||||
'An error occurred. Please try again.',
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles undefined error gracefully', async () => {
|
||||
mockSendMfaOtp.mockResolvedValue({
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.click(button);
|
||||
|
||||
// Should not throw an error
|
||||
await waitFor(() => {
|
||||
expect(mockSendMfaOtp).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MFA Ticket Renewal', () => {
|
||||
it('calls requestNewMfaTicket when ticket is invalid', async () => {
|
||||
// First call - set ticket as invalid
|
||||
mockSendMfaOtp.mockRejectedValueOnce({ message: 'Invalid ticket' });
|
||||
// Second call - should work
|
||||
mockSendMfaOtp.mockResolvedValueOnce({ success: true });
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
// First submission - creates error and marks ticket invalid
|
||||
await user.type(input, '123456');
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.toastError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Clear input and try again
|
||||
await user.clear(input);
|
||||
await user.type(input, '654321');
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRequestNewMfaTicket).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not call requestNewMfaTicket on first submission', async () => {
|
||||
mockSendMfaOtp.mockResolvedValue({ success: true });
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.click(button);
|
||||
|
||||
expect(mockRequestNewMfaTicket).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('works correctly when requestNewMfaTicket is not provided', async () => {
|
||||
const propsWithoutTicketRenewal = {
|
||||
sendMfaOtp: mockSendMfaOtp,
|
||||
loading: false,
|
||||
} as any;
|
||||
|
||||
mockSendMfaOtp.mockRejectedValueOnce({ message: 'Some error' });
|
||||
|
||||
render(<MfaOtpForm {...propsWithoutTicketRenewal} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.click(button);
|
||||
|
||||
// Should not throw an error even without requestNewMfaTicket
|
||||
await waitFor(() => {
|
||||
expect(mocks.toastError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('updates input value correctly when typing', async () => {
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
|
||||
await user.type(input, '123');
|
||||
expect(input).toHaveValue('123');
|
||||
|
||||
await user.type(input, '456');
|
||||
expect(input).toHaveValue('123456');
|
||||
});
|
||||
|
||||
it('can clear and retype input value', async () => {
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
|
||||
await user.type(input, '123456');
|
||||
expect(input).toHaveValue('123456');
|
||||
|
||||
await user.clear(input);
|
||||
expect(input).toHaveValue('');
|
||||
|
||||
await user.type(input, '654321');
|
||||
expect(input).toHaveValue('654321');
|
||||
});
|
||||
|
||||
it('button triggers submission with valid code', async () => {
|
||||
mockSendMfaOtp.mockResolvedValue({ success: true });
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.click(button);
|
||||
|
||||
expect(mockSendMfaOtp).toHaveBeenCalledWith('123456');
|
||||
});
|
||||
it('submits form when pressing Enter key with valid code', async () => {
|
||||
mockSendMfaOtp.mockResolvedValue({ success: true });
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.type(input, '{Enter}');
|
||||
|
||||
expect(mockSendMfaOtp).toHaveBeenCalledWith('123456');
|
||||
expect(mockSendMfaOtp).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not submit when pressing Enter with invalid code length', async () => {
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
|
||||
await user.type(input, '12345');
|
||||
await user.type(input, '{Enter}');
|
||||
|
||||
expect(mockSendMfaOtp).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not submit when pressing Enter while loading', async () => {
|
||||
render(<MfaOtpForm {...defaultProps} loading />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.type(input, '{Enter}');
|
||||
|
||||
expect(mockSendMfaOtp).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not submit multiple times when pressing Enter while submitting', async () => {
|
||||
let resolvePromise: (value: any) => void;
|
||||
const promise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
mockSendMfaOtp.mockReturnValue(promise);
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.type(input, '{Enter}');
|
||||
await user.type(input, '{Enter}'); // Second Enter should be ignored
|
||||
|
||||
expect(mockSendMfaOtp).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Clean up
|
||||
resolvePromise!({ success: true });
|
||||
await waitFor(async () => {
|
||||
await promise;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles null error message gracefully', async () => {
|
||||
mockSendMfaOtp.mockRejectedValueOnce({ message: null });
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '123456');
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.toastError).toHaveBeenCalledWith(
|
||||
'An error occurred. Please try again.',
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('prevents multiple rapid submissions', async () => {
|
||||
let resolvePromise: (value: any) => void;
|
||||
const promise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
mockSendMfaOtp.mockReturnValue(promise);
|
||||
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter TOTP');
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.type(input, '123456');
|
||||
|
||||
// Rapid clicks
|
||||
await user.click(button);
|
||||
await user.click(button);
|
||||
await user.click(button);
|
||||
|
||||
expect(mockSendMfaOtp).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Clean up
|
||||
resolvePromise!({ success: true });
|
||||
await waitFor(async () => {
|
||||
await promise;
|
||||
});
|
||||
});
|
||||
|
||||
it('handles empty input correctly', async () => {
|
||||
render(<MfaOtpForm {...defaultProps} />);
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Verify' });
|
||||
|
||||
await user.click(button);
|
||||
|
||||
expect(mockSendMfaOtp).not.toHaveBeenCalled();
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
87
dashboard/src/components/common/MfaOtpForm/MfaOtpForm.tsx
Normal file
87
dashboard/src/components/common/MfaOtpForm/MfaOtpForm.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { Input } from '@/components/ui/v3/input';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ChangeEvent,
|
||||
type KeyboardEvent,
|
||||
} from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface Props {
|
||||
sendMfaOtp: (code: string) => Promise<any>;
|
||||
loading: boolean;
|
||||
requestNewMfaTicket?: () => Promise<void>;
|
||||
}
|
||||
|
||||
function MfaOtpForm({ sendMfaOtp, loading, requestNewMfaTicket }: Props) {
|
||||
const [otpValue, setOtpValue] = useState<string>('');
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const isMfaTicketInvalid = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function submitTOTP() {
|
||||
if (otpValue.length === 6 && !isSubmitting) {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
if (requestNewMfaTicket && isMfaTicketInvalid.current) {
|
||||
await requestNewMfaTicket();
|
||||
}
|
||||
await sendMfaOtp(otpValue);
|
||||
} catch (error) {
|
||||
isMfaTicketInvalid.current = true;
|
||||
toast.error(
|
||||
error?.message || 'An error occurred. Please try again.',
|
||||
getToastStyleProps(),
|
||||
);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 10);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
const code = event.target.value.replace(/[^0-9]/g, '');
|
||||
setOtpValue(code);
|
||||
}
|
||||
|
||||
async function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.key === 'Enter') {
|
||||
submitTOTP();
|
||||
}
|
||||
}
|
||||
|
||||
const isInputDisabled = loading || isSubmitting;
|
||||
const isButtonDisabled = isInputDisabled || otpValue.length !== 6;
|
||||
|
||||
return (
|
||||
<div className="relative grid w-full grid-flow-row gap-4 bg-transparent">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={otpValue}
|
||||
placeholder="Enter TOTP"
|
||||
className="!bg-transparent"
|
||||
disabled={isInputDisabled}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<Button disabled={isButtonDisabled} onClick={submitTOTP}>
|
||||
{loading ? 'Verifying...' : 'Verify'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MfaOtpForm;
|
||||
1
dashboard/src/components/common/MfaOtpForm/index.ts
Normal file
1
dashboard/src/components/common/MfaOtpForm/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as MfaOtpForm } from './MfaOtpForm';
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { LinkProps } from '@/components/ui/v2/Link';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import type { MakeRequired } from '@/types/common';
|
||||
import NextLink from 'next/link';
|
||||
import type { ForwardedRef, PropsWithoutRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface NavLinkProps extends PropsWithoutRef<LinkProps> {
|
||||
export interface NavLinkProps
|
||||
extends MakeRequired<PropsWithoutRef<LinkProps>, 'href'> {
|
||||
/**
|
||||
* Determines whether or not the link should be disabled.
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useIsCurrentUserOwner } from '@/features/orgs/projects/common/hooks/use
|
||||
|
||||
interface Props {
|
||||
buttonText?: string;
|
||||
onClick?: () => void;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function OpenTransferDialogButton({ buttonText, onClick }: Props) {
|
||||
|
||||
@@ -19,7 +19,7 @@ export type PaginationProps = DetailedHTMLProps<
|
||||
/**
|
||||
* Number of total elements per page.
|
||||
*/
|
||||
elementsPerPage?: number;
|
||||
elementsPerPage: number;
|
||||
/**
|
||||
* Total number of elements.
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,6 @@ import { List } from '@/components/ui/v2/List';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import {} from '@/utils/__generated__/graphql';
|
||||
import { Divider } from '@mui/material';
|
||||
import debounce from 'lodash.debounce';
|
||||
import Image from 'next/image';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user