forked from cerc-io/snowballtools-base
Compare commits
77 Commits
05-22-svg_
...
main
Author | SHA1 | Date | |
---|---|---|---|
ea9a56eb65 | |||
05bd766133 | |||
0f18bc978e | |||
519e318190 | |||
63969ae25a | |||
b449c299dc | |||
2a35ec1cd5 | |||
be90fc76c1 | |||
3fa60f3cdf | |||
3d9aedeb7e | |||
096318cf13 | |||
27ef859075 | |||
5152952a45 | |||
ef26f9b39e | |||
d486f44cfe | |||
5c9c7575f2 | |||
59a164f3f8 | |||
bc52b34462 | |||
5aefda1248 | |||
42bdd21089 | |||
|
e5a00016c1 | ||
13b912d318 | |||
059863c4b9 | |||
58906844cc | |||
|
9f0a2ad548 | ||
|
bd10e2cb35 | ||
|
eb32385cf3 | ||
|
aebb20b987 | ||
|
8a2b51952f | ||
|
94f46f9621 | ||
|
e751addcce | ||
|
c01f8fdabf | ||
|
2b60114dab | ||
|
9a1c0e8338 | ||
|
1b038476c7 | ||
|
4a78eb13f6 | ||
|
41bcb2e7d0 | ||
|
f981f1a3f6 | ||
|
dee84f18cb | ||
|
44015d5451 | ||
|
a684743bd6 | ||
|
b12c95b2ff | ||
|
a4d9211ffe | ||
|
af31fac3ee | ||
|
54ae3f429d | ||
|
acfe78bf07 | ||
|
ce1833cb51 | ||
|
f2e59c11fd | ||
|
b261e7e436 | ||
|
934aa1a26b | ||
|
d975390a1b | ||
|
f77323364c | ||
|
c6a78f2116 | ||
|
cff9a5b2ea | ||
|
003b83ba21 | ||
|
82a1c151a8 | ||
|
2ada11f311 | ||
|
6e32d0678a | ||
|
198478f5fa | ||
552dfe783e | |||
|
e2bf5d052c | ||
33323b9bbf | |||
e2a3254563 | |||
8589bf4094 | |||
f8ebdfe7aa | |||
2166b2f800 | |||
cdd8d15e73 | |||
|
dc7b251988 | ||
|
bfb4a3f30b | ||
|
8f7fc888a9 | ||
|
0e9c3a07fd | ||
|
61e3e88a6c | ||
|
7b5ba1a5d0 | ||
|
b35f4033c5 | ||
|
306d3235b3 | ||
|
e148fd8d6b | ||
|
f84e2c0d9d |
29
.gitea/workflows/lint.yaml
Normal file
29
.gitea/workflows/lint.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20.x]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Download yarn
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o /usr/local/bin/yarn https://github.com/yarnpkg/yarn/releases/download/v1.22.21/yarn-1.22.21.js
|
||||||
|
chmod +x /usr/local/bin/yarn
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- run: yarn
|
||||||
|
- name: Build libs
|
||||||
|
run: yarn workspace gql-client run build
|
||||||
|
- name: Linter check
|
||||||
|
run: yarn lint
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -6,4 +6,7 @@ yarn-error.log
|
|||||||
|
|
||||||
packages/backend/environments/local.toml
|
packages/backend/environments/local.toml
|
||||||
packages/backend/dev/
|
packages/backend/dev/
|
||||||
packages/frontend/dist/
|
packages/frontend/dist/
|
||||||
|
|
||||||
|
# ignore all .DS_Store files
|
||||||
|
**/.DS_Store
|
||||||
|
223
README.md
223
README.md
@ -1,220 +1,23 @@
|
|||||||
# snowballtools-base
|
# snowballtools-base
|
||||||
|
|
||||||
## Setup
|
This is a [yarn workspace](https://yarnpkg.com/features/workspaces) monorepo for the dashboard.
|
||||||
|
|
||||||
- Clone the `snowballtools-base` repo
|
## Getting Started
|
||||||
|
|
||||||
```bash
|
### Install dependencies
|
||||||
git clone git@github.com:snowball-tools/snowballtools-base.git
|
|
||||||
cd snowballtools-base
|
|
||||||
```
|
|
||||||
|
|
||||||
- Install dependencies in root
|
In the root of the project, run:
|
||||||
|
|
||||||
```bash
|
```zsh
|
||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
- Build packages
|
### Build backend
|
||||||
|
|
||||||
```bash
|
```zsh
|
||||||
yarn build --ignore frontend
|
yarn build --ignore frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
Let us assume the following domains for backend and frontend
|
### Environment variables, running the development server, and deployment
|
||||||
- Backend server: `api.snowballtools.com`
|
|
||||||
- Frontend app: `dashboard.snowballtools.com`
|
|
||||||
|
|
||||||
- Configuration Files
|
Follow the instructions in the README.md files of the [backend](packages/backend/README.md) and [frontend](packages/frontend/README.md) packages.
|
||||||
|
|
||||||
- For Backend:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp packages/backend/environments/local.toml.example packages/backend/environments/local.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
- Production
|
|
||||||
|
|
||||||
- Update the following in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[server]
|
|
||||||
...
|
|
||||||
[server.session]
|
|
||||||
# Secret should be changed to a different random string
|
|
||||||
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7sidnc382"
|
|
||||||
# Set URL of the frontend app
|
|
||||||
appOriginUrl = "https://dashboard.snowballtools.com"
|
|
||||||
# Set to true for session cookies to work behind proxy
|
|
||||||
trustProxy = true
|
|
||||||
# Set empty domain when using secure connection
|
|
||||||
domain = ""
|
|
||||||
```
|
|
||||||
|
|
||||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
|
|
||||||
- Homepage URL: `https://dashboard.snowballtools.com`
|
|
||||||
- Authorization callback URL: `https://dashboard.snowballtools.com/organization/projects/create`
|
|
||||||
- Generate a new client secret after app is created
|
|
||||||
|
|
||||||
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[gitHub]
|
|
||||||
webhookUrl = "https://api.snowballtools.com"
|
|
||||||
```
|
|
||||||
|
|
||||||
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[registryConfig]
|
|
||||||
fetchDeploymentRecordDelay = 5000
|
|
||||||
# Use actual port for REST endpoint
|
|
||||||
restEndpoint = "http://api.laconic.com:1317"
|
|
||||||
# Use actual port for GQL endpoint
|
|
||||||
gqlEndpoint = "http://api.laconic.com:9473/api"
|
|
||||||
# Set private key of account to be used in Laconicd
|
|
||||||
privateKey = "0wtu92cd4f1y791ezpjwgzzazni4dmd3q3mzqc3t6i6r9v06ji784tey6hwmnn69"
|
|
||||||
# Set Bond ID to be used for publishing records
|
|
||||||
bondId = "8xk8c2pb61kajwixpm223zvptr2x2ncajq0vd998p6aqhvqqep2reu6pik245epf"
|
|
||||||
chainId = "laconic_9000-1"
|
|
||||||
# Set authority that is existing in the chain
|
|
||||||
authority = "laconic"
|
|
||||||
[registryConfig.fee]
|
|
||||||
amount = "200000"
|
|
||||||
denom = "aphoton"
|
|
||||||
gas = "750000"
|
|
||||||
```
|
|
||||||
|
|
||||||
- Development
|
|
||||||
|
|
||||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
|
|
||||||
- Homepage URL: `http://localhost:3000`
|
|
||||||
- Authorization callback URL: `http://localhost:3000/organization/projects/create`
|
|
||||||
- Generate a new client secret after app is created
|
|
||||||
|
|
||||||
- Setup Laconicd
|
|
||||||
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
|
|
||||||
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
laconic-so deployment --dir laconic-loaded-deployment exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
|
|
||||||
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
|
|
||||||
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
|
|
||||||
```
|
|
||||||
|
|
||||||
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
- Run the script to create bond, reserve the authority and set authority bond
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn test:registry:init
|
|
||||||
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
|
|
||||||
```
|
|
||||||
|
|
||||||
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
- Setup ngrok for GitHub webhooks
|
|
||||||
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
|
|
||||||
- Start ngrok and point to backend server endpoint
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ngrok http http://localhost:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
- Look for the forwarding URL in ngrok
|
|
||||||
|
|
||||||
```bash
|
|
||||||
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[gitHub]
|
|
||||||
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
|
|
||||||
```
|
|
||||||
|
|
||||||
- For Frontend:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp packages/frontend/.env.example packages/frontend/.env
|
|
||||||
```
|
|
||||||
|
|
||||||
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_GITHUB_CLIENT_ID = <CLIENT_ID>
|
|
||||||
```
|
|
||||||
|
|
||||||
- Set `VITE_GITHUB_PWA_TEMPLATE_REPO` and `VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_GITHUB_PWA_TEMPLATE_REPO = 'cerc-io/test-progressive-web-app' # Set actual owner/name of the template repo that will be used for creating new repo
|
|
||||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'cerc-io/image-upload-pwa-example' # Set actual owner/name of the template repo that will be used for creating new repo
|
|
||||||
```
|
|
||||||
|
|
||||||
- Production
|
|
||||||
|
|
||||||
- Set the following values in [.env](packages/frontend/.env) file
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_SERVER_URL = 'https://api.snowballtools.com' # Backend server endpoint
|
|
||||||
```
|
|
||||||
|
|
||||||
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
|
|
||||||
- Create a project and add information to use wallet connect SDK
|
|
||||||
- Add project name and select project type as `App`
|
|
||||||
- Set project home page URL to `https://dashboard.snowballtools.com`
|
|
||||||
- On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_WALLET_CONNECT_ID = <PROJECT_ID>
|
|
||||||
```
|
|
||||||
|
|
||||||
- Build the React application
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
- Use a web server for hosting static built files
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 -m http.server -d build 3000
|
|
||||||
```
|
|
||||||
|
|
||||||
- Development
|
|
||||||
|
|
||||||
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_SERVER_URL = 'http://localhost:8000'
|
|
||||||
```
|
|
||||||
|
|
||||||
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID.
|
|
||||||
- Create a project and add information to use wallet connect SDK
|
|
||||||
- Add project name and select project type as `App`
|
|
||||||
- Project home page URL is not required to be set
|
|
||||||
- On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_WALLET_CONNECT_ID = <Project_ID>
|
|
||||||
```
|
|
||||||
|
|
||||||
- The React application will be running in `http://localhost:3000/`
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
- Start the backend server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn start:backend
|
|
||||||
```
|
|
||||||
|
|
||||||
- Start the frontend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn start
|
|
||||||
```
|
|
||||||
|
@ -15,8 +15,8 @@ VITE_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_github_clientid'
|
|||||||
VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo'
|
VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo'
|
||||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo'
|
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo'
|
||||||
VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id'
|
VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id'
|
||||||
|
VITE_LACONICD_CHAIN_ID = 'LACONIC_HOSTED_CONFIG_laconicd_chain_id'
|
||||||
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
|
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
|
||||||
VITE_ALCHEMY_API_KEY = 'LACONIC_HOSTED_CONFIG_aplchemy_api_key'
|
|
||||||
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'
|
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'
|
||||||
VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid'
|
VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid'
|
||||||
VITE_TURNKEY_API_BASE_URL = 'LACONIC_HOSTED_CONFIG_turnkey_api_base_url'
|
VITE_TURNKEY_API_BASE_URL = 'LACONIC_HOSTED_CONFIG_turnkey_api_base_url'
|
||||||
@ -24,7 +24,7 @@ VITE_TURNKEY_ORGANIZATION_ID = 'LACONIC_HOSTED_CONFIG_turnkey_organization_id'
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
yarn || exit 1
|
yarn || exit 1
|
||||||
yarn build || exit 1
|
yarn build --ignore backend || exit 1
|
||||||
|
|
||||||
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
||||||
echo "Missing output directory: $OUTPUT_DIR" 1>&2
|
echo "Missing output directory: $OUTPUT_DIR" 1>&2
|
||||||
|
@ -1 +1,76 @@
|
|||||||
# Backend for Snowball Tools
|
# backend
|
||||||
|
|
||||||
|
This backend is a [node.js](https://nodejs.org/) [express.js](https://expressjs.com/) [apollo server](https://www.apollographql.com/docs/apollo-server/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
|
||||||
|
In the root of the project, run:
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build backend
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
yarn build --ignore frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
#### Local
|
||||||
|
|
||||||
|
Copy the `environments/local.toml.example` file to `environments/local.toml`:
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
cp environments/local.toml.example environments/local.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Staging environment variables
|
||||||
|
|
||||||
|
In the deployment repository, update staging [staging/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/staging/configmaps/config/prod.toml)
|
||||||
|
|
||||||
|
#### Production environment variables
|
||||||
|
|
||||||
|
In the deployment repository, update production [production/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/production/configmaps/config/prod.toml)
|
||||||
|
|
||||||
|
### Run development server
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Clone the [deployer repository](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments):
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Staging
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
echo trigger >> .gitea/workflows/triggers/staging-deploy
|
||||||
|
git commit -a -m "Deploy v0.0.8" # replace with version number
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
echo trigger >> .gitea/workflows/triggers/production-deploy
|
||||||
|
git commit -a -m "Deploy v0.0.8" # replace with version number
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment status
|
||||||
|
|
||||||
|
Dumb for now
|
||||||
|
|
||||||
|
- [Staging](https://snowballtools-base-api.staging.apps.snowballtools.com/staging/version)
|
||||||
|
- [Production](https://snowballtools-base-api.apps.snowballtools.com/staging/version)
|
||||||
|
|
||||||
|
Update version number manually in [routes/staging.ts](/packages/backend/src/routes/staging.ts)
|
||||||
|
@ -3,9 +3,12 @@
|
|||||||
port = 8000
|
port = 8000
|
||||||
gqlPath = "/graphql"
|
gqlPath = "/graphql"
|
||||||
[server.session]
|
[server.session]
|
||||||
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi"
|
secret = ""
|
||||||
|
# Frontend webapp URL origin
|
||||||
appOriginUrl = "http://localhost:3000"
|
appOriginUrl = "http://localhost:3000"
|
||||||
|
# Set to true if server running behind proxy
|
||||||
trustProxy = false
|
trustProxy = false
|
||||||
|
# Backend URL hostname
|
||||||
domain = "localhost"
|
domain = "localhost"
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
@ -19,6 +22,7 @@
|
|||||||
|
|
||||||
[registryConfig]
|
[registryConfig]
|
||||||
fetchDeploymentRecordDelay = 5000
|
fetchDeploymentRecordDelay = 5000
|
||||||
|
checkAuctionStatusDelay = 5000
|
||||||
restEndpoint = "http://localhost:1317"
|
restEndpoint = "http://localhost:1317"
|
||||||
gqlEndpoint = "http://localhost:9473/api"
|
gqlEndpoint = "http://localhost:9473/api"
|
||||||
chainId = "laconic_9000-1"
|
chainId = "laconic_9000-1"
|
||||||
@ -26,9 +30,14 @@
|
|||||||
bondId = ""
|
bondId = ""
|
||||||
authority = ""
|
authority = ""
|
||||||
[registryConfig.fee]
|
[registryConfig.fee]
|
||||||
amount = "200000"
|
gas = ""
|
||||||
denom = "aphoton"
|
fees = ""
|
||||||
gas = "750000"
|
gasPrice = "1alnt"
|
||||||
|
|
||||||
[misc]
|
# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions
|
||||||
projectDomain = "apps.snowballtools.com"
|
[auction]
|
||||||
|
commitFee = "100000"
|
||||||
|
commitsDuration = "120s"
|
||||||
|
revealFee = "100000"
|
||||||
|
revealsDuration = "120s"
|
||||||
|
denom = "alnt"
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@cerc-io/registry-sdk": "^0.2.11",
|
||||||
"@graphql-tools/schema": "^10.0.2",
|
"@graphql-tools/schema": "^10.0.2",
|
||||||
"@graphql-tools/utils": "^10.0.12",
|
"@graphql-tools/utils": "^10.0.12",
|
||||||
"@octokit/oauth-app": "^6.1.0",
|
"@octokit/oauth-app": "^6.1.0",
|
||||||
"@snowballtools/laconic-sdk": "^0.1.17",
|
|
||||||
"@turnkey/sdk-server": "^0.1.0",
|
"@turnkey/sdk-server": "^0.1.0",
|
||||||
"@types/debug": "^4.1.5",
|
"@types/debug": "^4.1.5",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
@ -19,6 +19,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-async-errors": "^3.1.1",
|
||||||
"express-session": "^1.18.0",
|
"express-session": "^1.18.0",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
|
@ -34,16 +34,21 @@ export interface RegistryConfig {
|
|||||||
privateKey: string;
|
privateKey: string;
|
||||||
bondId: string;
|
bondId: string;
|
||||||
fetchDeploymentRecordDelay: number;
|
fetchDeploymentRecordDelay: number;
|
||||||
|
checkAuctionStatusDelay: number;
|
||||||
authority: string;
|
authority: string;
|
||||||
fee: {
|
fee: {
|
||||||
amount: string;
|
|
||||||
denom: string;
|
|
||||||
gas: string;
|
gas: string;
|
||||||
|
fees: string;
|
||||||
|
gasPrice: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MiscConfig {
|
export interface AuctionConfig {
|
||||||
projectDomain: string;
|
commitFee: string;
|
||||||
|
commitsDuration: string;
|
||||||
|
revealFee: string;
|
||||||
|
revealsDuration: string;
|
||||||
|
denom: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
@ -51,7 +56,7 @@ export interface Config {
|
|||||||
database: DatabaseConfig;
|
database: DatabaseConfig;
|
||||||
gitHub: GitHubConfig;
|
gitHub: GitHubConfig;
|
||||||
registryConfig: RegistryConfig;
|
registryConfig: RegistryConfig;
|
||||||
misc: MiscConfig;
|
auction: AuctionConfig;
|
||||||
turnkey: {
|
turnkey: {
|
||||||
apiBaseUrl: string;
|
apiBaseUrl: string;
|
||||||
apiPublicKey: string;
|
apiPublicKey: string;
|
||||||
|
@ -3,7 +3,9 @@ import {
|
|||||||
DeepPartial,
|
DeepPartial,
|
||||||
FindManyOptions,
|
FindManyOptions,
|
||||||
FindOneOptions,
|
FindOneOptions,
|
||||||
FindOptionsWhere
|
FindOptionsWhere,
|
||||||
|
IsNull,
|
||||||
|
Not
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
@ -11,7 +13,7 @@ import assert from 'assert';
|
|||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
import { lowercase, numbers } from 'nanoid-dictionary';
|
import { lowercase, numbers } from 'nanoid-dictionary';
|
||||||
|
|
||||||
import { DatabaseConfig, MiscConfig } from './config';
|
import { DatabaseConfig } from './config';
|
||||||
import { User } from './entity/User';
|
import { User } from './entity/User';
|
||||||
import { Organization } from './entity/Organization';
|
import { Organization } from './entity/Organization';
|
||||||
import { Project } from './entity/Project';
|
import { Project } from './entity/Project';
|
||||||
@ -21,6 +23,7 @@ import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
|||||||
import { Domain } from './entity/Domain';
|
import { Domain } from './entity/Domain';
|
||||||
import { getEntities, loadAndSaveData } from './utils';
|
import { getEntities, loadAndSaveData } from './utils';
|
||||||
import { UserOrganization } from './entity/UserOrganization';
|
import { UserOrganization } from './entity/UserOrganization';
|
||||||
|
import { Deployer } from './entity/Deployer';
|
||||||
|
|
||||||
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
||||||
|
|
||||||
@ -31,9 +34,8 @@ const nanoid = customAlphabet(lowercase + numbers, 8);
|
|||||||
// TODO: Fix order of methods
|
// TODO: Fix order of methods
|
||||||
export class Database {
|
export class Database {
|
||||||
private dataSource: DataSource;
|
private dataSource: DataSource;
|
||||||
private projectDomain: string;
|
|
||||||
|
|
||||||
constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) {
|
constructor({ dbPath }: DatabaseConfig) {
|
||||||
this.dataSource = new DataSource({
|
this.dataSource = new DataSource({
|
||||||
type: 'better-sqlite3',
|
type: 'better-sqlite3',
|
||||||
database: dbPath,
|
database: dbPath,
|
||||||
@ -41,11 +43,9 @@ export class Database {
|
|||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: false
|
logging: false
|
||||||
});
|
});
|
||||||
|
|
||||||
this.projectDomain = projectDomain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init (): Promise<void> {
|
async init(): Promise<void> {
|
||||||
await this.dataSource.initialize();
|
await this.dataSource.initialize();
|
||||||
log('database initialized');
|
log('database initialized');
|
||||||
|
|
||||||
@ -58,21 +58,21 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser (options: FindOneOptions<User>): Promise<User | null> {
|
async getUser(options: FindOneOptions<User>): Promise<User | null> {
|
||||||
const userRepository = this.dataSource.getRepository(User);
|
const userRepository = this.dataSource.getRepository(User);
|
||||||
const user = await userRepository.findOne(options);
|
const user = await userRepository.findOne(options);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addUser (data: DeepPartial<User>): Promise<User> {
|
async addUser(data: DeepPartial<User>): Promise<User> {
|
||||||
const userRepository = this.dataSource.getRepository(User);
|
const userRepository = this.dataSource.getRepository(User);
|
||||||
const user = await userRepository.save(data);
|
const user = await userRepository.save(data);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser (user: User, data: DeepPartial<User>): Promise<boolean> {
|
async updateUser(user: User, data: DeepPartial<User>): Promise<boolean> {
|
||||||
const userRepository = this.dataSource.getRepository(User);
|
const userRepository = this.dataSource.getRepository(User);
|
||||||
const updateResult = await userRepository.update({ id: user.id }, data);
|
const updateResult = await userRepository.update({ id: user.id }, data);
|
||||||
assert(updateResult.affected);
|
assert(updateResult.affected);
|
||||||
@ -80,7 +80,7 @@ export class Database {
|
|||||||
return updateResult.affected > 0;
|
return updateResult.affected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganizations (
|
async getOrganizations(
|
||||||
options: FindManyOptions<Organization>
|
options: FindManyOptions<Organization>
|
||||||
): Promise<Organization[]> {
|
): Promise<Organization[]> {
|
||||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
@ -89,7 +89,7 @@ export class Database {
|
|||||||
return organizations;
|
return organizations;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganization (
|
async getOrganization(
|
||||||
options: FindOneOptions<Organization>
|
options: FindOneOptions<Organization>
|
||||||
): Promise<Organization | null> {
|
): Promise<Organization | null> {
|
||||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
@ -98,7 +98,7 @@ export class Database {
|
|||||||
return organization;
|
return organization;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
|
async getOrganizationsByUserId(userId: string): Promise<Organization[]> {
|
||||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
|
|
||||||
const userOrgs = await organizationRepository.find({
|
const userOrgs = await organizationRepository.find({
|
||||||
@ -114,21 +114,21 @@ export class Database {
|
|||||||
return userOrgs;
|
return userOrgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addUserOrganization (data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
|
async addUserOrganization(data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
|
||||||
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
|
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
|
||||||
const newUserOrganization = await userOrganizationRepository.save(data);
|
const newUserOrganization = await userOrganizationRepository.save(data);
|
||||||
|
|
||||||
return newUserOrganization;
|
return newUserOrganization;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects (options: FindManyOptions<Project>): Promise<Project[]> {
|
async getProjects(options: FindManyOptions<Project>): Promise<Project[]> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
const projects = await projectRepository.find(options);
|
const projects = await projectRepository.find(options);
|
||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectById (projectId: string): Promise<Project | null> {
|
async getProjectById(projectId: string): Promise<Project | null> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
const project = await projectRepository
|
const project = await projectRepository
|
||||||
@ -140,7 +140,9 @@ export class Database {
|
|||||||
)
|
)
|
||||||
.leftJoinAndSelect('deployments.createdBy', 'user')
|
.leftJoinAndSelect('deployments.createdBy', 'user')
|
||||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||||
|
.leftJoinAndSelect('deployments.deployer', 'deployer')
|
||||||
.leftJoinAndSelect('project.owner', 'owner')
|
.leftJoinAndSelect('project.owner', 'owner')
|
||||||
|
.leftJoinAndSelect('project.deployers', 'deployers')
|
||||||
.leftJoinAndSelect('project.organization', 'organization')
|
.leftJoinAndSelect('project.organization', 'organization')
|
||||||
.where('project.id = :projectId', {
|
.where('project.id = :projectId', {
|
||||||
projectId
|
projectId
|
||||||
@ -150,7 +152,25 @@ export class Database {
|
|||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsInOrganization (
|
async allProjectsWithoutDeployments(): Promise<Project[]> {
|
||||||
|
const allProjects = await this.getProjects({
|
||||||
|
where: {
|
||||||
|
auctionId: Not(IsNull()),
|
||||||
|
},
|
||||||
|
relations: ['deployments'],
|
||||||
|
withDeleted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const projects = allProjects.filter(project => {
|
||||||
|
if (project.deletedAt !== null) return false;
|
||||||
|
|
||||||
|
return project.deployments.length === 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjectsInOrganization(
|
||||||
userId: string,
|
userId: string,
|
||||||
organizationSlug: string
|
organizationSlug: string
|
||||||
): Promise<Project[]> {
|
): Promise<Project[]> {
|
||||||
@ -181,7 +201,7 @@ export class Database {
|
|||||||
/**
|
/**
|
||||||
* Get deployments with specified filter
|
* Get deployments with specified filter
|
||||||
*/
|
*/
|
||||||
async getDeployments (
|
async getDeployments(
|
||||||
options: FindManyOptions<Deployment>
|
options: FindManyOptions<Deployment>
|
||||||
): Promise<Deployment[]> {
|
): Promise<Deployment[]> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
@ -190,12 +210,13 @@ export class Database {
|
|||||||
return deployments;
|
return deployments;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
|
async getDeploymentsByProjectId(projectId: string): Promise<Deployment[]> {
|
||||||
return this.getDeployments({
|
return this.getDeployments({
|
||||||
relations: {
|
relations: {
|
||||||
project: true,
|
project: true,
|
||||||
domain: true,
|
domain: true,
|
||||||
createdBy: true
|
createdBy: true,
|
||||||
|
deployer: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
project: {
|
project: {
|
||||||
@ -208,7 +229,7 @@ export class Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeployment (
|
async getDeployment(
|
||||||
options: FindOneOptions<Deployment>
|
options: FindOneOptions<Deployment>
|
||||||
): Promise<Deployment | null> {
|
): Promise<Deployment | null> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
@ -217,14 +238,14 @@ export class Database {
|
|||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDomains (options: FindManyOptions<Domain>): Promise<Domain[]> {
|
async getDomains(options: FindManyOptions<Domain>): Promise<Domain[]> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
const domains = await domainRepository.find(options);
|
const domains = await domainRepository.find(options);
|
||||||
|
|
||||||
return domains;
|
return domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDeployment (data: DeepPartial<Deployment>): Promise<Deployment> {
|
async addDeployment(data: DeepPartial<Deployment>): Promise<Deployment> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
|
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
@ -238,7 +259,7 @@ export class Database {
|
|||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMembersByProjectId (
|
async getProjectMembersByProjectId(
|
||||||
projectId: string
|
projectId: string
|
||||||
): Promise<ProjectMember[]> {
|
): Promise<ProjectMember[]> {
|
||||||
const projectMemberRepository =
|
const projectMemberRepository =
|
||||||
@ -259,7 +280,7 @@ export class Database {
|
|||||||
return projectMembers;
|
return projectMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEnvironmentVariablesByProjectId (
|
async getEnvironmentVariablesByProjectId(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
filter?: FindOptionsWhere<EnvironmentVariable>
|
filter?: FindOptionsWhere<EnvironmentVariable>
|
||||||
): Promise<EnvironmentVariable[]> {
|
): Promise<EnvironmentVariable[]> {
|
||||||
@ -278,7 +299,7 @@ export class Database {
|
|||||||
return environmentVariables;
|
return environmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
|
async removeProjectMemberById(projectMemberId: string): Promise<boolean> {
|
||||||
const projectMemberRepository =
|
const projectMemberRepository =
|
||||||
this.dataSource.getRepository(ProjectMember);
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
@ -293,7 +314,7 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectMemberById (
|
async updateProjectMemberById(
|
||||||
projectMemberId: string,
|
projectMemberId: string,
|
||||||
data: DeepPartial<ProjectMember>
|
data: DeepPartial<ProjectMember>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -307,7 +328,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProjectMember (
|
async addProjectMember(
|
||||||
data: DeepPartial<ProjectMember>
|
data: DeepPartial<ProjectMember>
|
||||||
): Promise<ProjectMember> {
|
): Promise<ProjectMember> {
|
||||||
const projectMemberRepository =
|
const projectMemberRepository =
|
||||||
@ -317,7 +338,7 @@ export class Database {
|
|||||||
return newProjectMember;
|
return newProjectMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addEnvironmentVariables (
|
async addEnvironmentVariables(
|
||||||
data: DeepPartial<EnvironmentVariable>[]
|
data: DeepPartial<EnvironmentVariable>[]
|
||||||
): Promise<EnvironmentVariable[]> {
|
): Promise<EnvironmentVariable[]> {
|
||||||
const environmentVariableRepository =
|
const environmentVariableRepository =
|
||||||
@ -328,7 +349,7 @@ export class Database {
|
|||||||
return savedEnvironmentVariables;
|
return savedEnvironmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEnvironmentVariable (
|
async updateEnvironmentVariable(
|
||||||
environmentVariableId: string,
|
environmentVariableId: string,
|
||||||
data: DeepPartial<EnvironmentVariable>
|
data: DeepPartial<EnvironmentVariable>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -342,7 +363,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteEnvironmentVariable (
|
async deleteEnvironmentVariable(
|
||||||
environmentVariableId: string
|
environmentVariableId: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const environmentVariableRepository =
|
const environmentVariableRepository =
|
||||||
@ -358,7 +379,7 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
|
async getProjectMemberById(projectMemberId: string): Promise<ProjectMember> {
|
||||||
const projectMemberRepository =
|
const projectMemberRepository =
|
||||||
this.dataSource.getRepository(ProjectMember);
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
@ -381,7 +402,7 @@ export class Database {
|
|||||||
return projectMemberWithProject[0];
|
return projectMemberWithProject[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsBySearchText (
|
async getProjectsBySearchText(
|
||||||
userId: string,
|
userId: string,
|
||||||
searchText: string
|
searchText: string
|
||||||
): Promise<Project[]> {
|
): Promise<Project[]> {
|
||||||
@ -403,14 +424,14 @@ export class Database {
|
|||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentById (
|
async updateDeploymentById(
|
||||||
deploymentId: string,
|
deploymentId: string,
|
||||||
data: DeepPartial<Deployment>
|
data: DeepPartial<Deployment>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
return this.updateDeployment({ id: deploymentId }, data);
|
return this.updateDeployment({ id: deploymentId }, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeployment (
|
async updateDeployment(
|
||||||
criteria: FindOptionsWhere<Deployment>,
|
criteria: FindOptionsWhere<Deployment>,
|
||||||
data: DeepPartial<Deployment>
|
data: DeepPartial<Deployment>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -420,7 +441,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentsByProjectIds (
|
async updateDeploymentsByProjectIds(
|
||||||
projectIds: string[],
|
projectIds: string[],
|
||||||
data: DeepPartial<Deployment>
|
data: DeepPartial<Deployment>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -436,7 +457,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteDeploymentById (deploymentId: string): Promise<boolean> {
|
async deleteDeploymentById(deploymentId: string): Promise<boolean> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
const deployment = await deploymentRepository.findOneOrFail({
|
const deployment = await deploymentRepository.findOneOrFail({
|
||||||
where: {
|
where: {
|
||||||
@ -449,7 +470,7 @@ export class Database {
|
|||||||
return Boolean(deleteResult);
|
return Boolean(deleteResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProject (user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
|
async addProject(user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
// TODO: Check if organization exists
|
// TODO: Check if organization exists
|
||||||
@ -465,12 +486,16 @@ export class Database {
|
|||||||
id: organizationId
|
id: organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
|
|
||||||
|
|
||||||
return projectRepository.save(newProject);
|
return projectRepository.save(newProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectById (
|
async saveProject(project: Project): Promise<Project> {
|
||||||
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
|
return projectRepository.save(project);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProjectById(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
data: DeepPartial<Project>
|
data: DeepPartial<Project>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -483,7 +508,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteProjectById (projectId: string): Promise<boolean> {
|
async deleteProjectById(projectId: string): Promise<boolean> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
const project = await projectRepository.findOneOrFail({
|
const project = await projectRepository.findOneOrFail({
|
||||||
where: {
|
where: {
|
||||||
@ -499,7 +524,7 @@ export class Database {
|
|||||||
return Boolean(deleteResult);
|
return Boolean(deleteResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteDomainById (domainId: string): Promise<boolean> {
|
async deleteDomainById(domainId: string): Promise<boolean> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
|
|
||||||
const deleteResult = await domainRepository.softDelete({ id: domainId });
|
const deleteResult = await domainRepository.softDelete({ id: domainId });
|
||||||
@ -511,21 +536,21 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDomain (data: DeepPartial<Domain>): Promise<Domain> {
|
async addDomain(data: DeepPartial<Domain>): Promise<Domain> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
const newDomain = await domainRepository.save(data);
|
const newDomain = await domainRepository.save(data);
|
||||||
|
|
||||||
return newDomain;
|
return newDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDomain (options: FindOneOptions<Domain>): Promise<Domain | null> {
|
async getDomain(options: FindOneOptions<Domain>): Promise<Domain | null> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
const domain = await domainRepository.findOne(options);
|
const domain = await domainRepository.findOne(options);
|
||||||
|
|
||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDomainById (
|
async updateDomainById(
|
||||||
domainId: string,
|
domainId: string,
|
||||||
data: DeepPartial<Domain>
|
data: DeepPartial<Domain>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -535,7 +560,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDomainsByProjectId (
|
async getDomainsByProjectId(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
filter?: FindOptionsWhere<Domain>
|
filter?: FindOptionsWhere<Domain>
|
||||||
): Promise<Domain[]> {
|
): Promise<Domain[]> {
|
||||||
@ -555,4 +580,24 @@ export class Database {
|
|||||||
|
|
||||||
return domains;
|
return domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addDeployer(data: DeepPartial<Deployer>): Promise<Deployer> {
|
||||||
|
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||||
|
const newDomain = await deployerRepository.save(data);
|
||||||
|
|
||||||
|
return newDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeployers(): Promise<Deployer[]> {
|
||||||
|
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||||
|
const deployers = await deployerRepository.find();
|
||||||
|
return deployers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeployerByLRN(deployerLrn: string): Promise<Deployer | null> {
|
||||||
|
const deployerRepository = this.dataSource.getRepository(Deployer);
|
||||||
|
const deployer = await deployerRepository.findOne({ where: { deployerLrn } });
|
||||||
|
|
||||||
|
return deployer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
26
packages/backend/src/entity/Deployer.ts
Normal file
26
packages/backend/src/entity/Deployer.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Entity, PrimaryColumn, Column, ManyToMany } from 'typeorm';
|
||||||
|
import { Project } from './Project';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Deployer {
|
||||||
|
@PrimaryColumn('varchar')
|
||||||
|
deployerLrn!: string;
|
||||||
|
|
||||||
|
@Column('varchar')
|
||||||
|
deployerId!: string;
|
||||||
|
|
||||||
|
@Column('varchar')
|
||||||
|
deployerApiUrl!: string;
|
||||||
|
|
||||||
|
@Column('varchar')
|
||||||
|
baseDomain!: string;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
minimumPayment!: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
paymentAddress!: string | null;
|
||||||
|
|
||||||
|
@ManyToMany(() => Project, (project) => project.deployers)
|
||||||
|
projects!: Project[];
|
||||||
|
}
|
@ -13,6 +13,7 @@ import {
|
|||||||
import { Project } from './Project';
|
import { Project } from './Project';
|
||||||
import { Domain } from './Domain';
|
import { Domain } from './Domain';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
|
import { Deployer } from './Deployer';
|
||||||
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types';
|
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types';
|
||||||
|
|
||||||
export enum Environment {
|
export enum Environment {
|
||||||
@ -33,21 +34,21 @@ export interface ApplicationDeploymentRequest {
|
|||||||
version: string;
|
version: string;
|
||||||
name: string;
|
name: string;
|
||||||
application: string;
|
application: string;
|
||||||
|
lrn?: string;
|
||||||
|
auction?: string;
|
||||||
config: string;
|
config: string;
|
||||||
meta: string;
|
meta: string;
|
||||||
|
payment?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApplicationDeploymentRemovalRequest {
|
export interface ApplicationDeploymentRemovalRequest {
|
||||||
type: string;
|
type: string;
|
||||||
version: string;
|
version: string;
|
||||||
deployment: string;
|
deployment: string;
|
||||||
|
auction?: string;
|
||||||
|
payment?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApplicationDeploymentRemovalRequest {
|
|
||||||
type: string;
|
|
||||||
version: string;
|
|
||||||
deployment: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApplicationRecord {
|
export interface ApplicationRecord {
|
||||||
type: string;
|
type: string;
|
||||||
@ -112,19 +113,23 @@ export class Deployment {
|
|||||||
|
|
||||||
@Column('simple-json', { nullable: true })
|
@Column('simple-json', { nullable: true })
|
||||||
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
|
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
applicationDeploymentRemovalRequestId!: string | null;
|
applicationDeploymentRemovalRequestId!: string | null;
|
||||||
|
|
||||||
@Column('simple-json', { nullable: true })
|
@Column('simple-json', { nullable: true })
|
||||||
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null;
|
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null;
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
applicationDeploymentRemovalRecordId!: string | null;
|
applicationDeploymentRemovalRecordId!: string | null;
|
||||||
|
|
||||||
@Column('simple-json', { nullable: true })
|
@Column('simple-json', { nullable: true })
|
||||||
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
|
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => Deployer)
|
||||||
|
@JoinColumn({ name: 'deployerLrn' })
|
||||||
|
deployer!: Deployer;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
enum: Environment
|
enum: Environment
|
||||||
})
|
})
|
||||||
@ -147,7 +152,7 @@ export class Deployment {
|
|||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@DeleteDateColumn()
|
@DeleteDateColumn()
|
||||||
deletedAt!: Date | null;
|
deletedAt!: Date | null;
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,16 @@ import {
|
|||||||
ManyToOne,
|
ManyToOne,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
DeleteDateColumn
|
DeleteDateColumn,
|
||||||
|
JoinTable,
|
||||||
|
ManyToMany
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
import { Organization } from './Organization';
|
import { Organization } from './Organization';
|
||||||
import { ProjectMember } from './ProjectMember';
|
import { ProjectMember } from './ProjectMember';
|
||||||
import { Deployment } from './Deployment';
|
import { Deployment } from './Deployment';
|
||||||
|
import { Deployer } from './Deployer';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Project {
|
export class Project {
|
||||||
@ -46,6 +49,20 @@ export class Project {
|
|||||||
@Column('text', { default: '' })
|
@Column('text', { default: '' })
|
||||||
description!: string;
|
description!: string;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
auctionId!: string | null;
|
||||||
|
|
||||||
|
// Tx hash for sending coins from snowball to deployer
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
txHash!: string | null;
|
||||||
|
|
||||||
|
@ManyToMany(() => Deployer, (deployer) => (deployer.projects))
|
||||||
|
@JoinTable()
|
||||||
|
deployers!: Deployer[]
|
||||||
|
|
||||||
|
@Column('boolean', { default: false, nullable: true })
|
||||||
|
fundsReleased!: boolean;
|
||||||
|
|
||||||
// TODO: Compute template & framework in import repository
|
// TODO: Compute template & framework in import repository
|
||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
template!: string | null;
|
template!: string | null;
|
||||||
@ -53,6 +70,10 @@ export class Project {
|
|||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
framework!: string | null;
|
framework!: string | null;
|
||||||
|
|
||||||
|
// Address of the user who created the project i.e. requested deployments
|
||||||
|
@Column('varchar')
|
||||||
|
paymentAddress!: string;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: 'simple-array'
|
type: 'simple-array'
|
||||||
})
|
})
|
||||||
@ -61,9 +82,6 @@ export class Project {
|
|||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
icon!: string;
|
icon!: string;
|
||||||
|
|
||||||
@Column('varchar')
|
|
||||||
subDomain!: string;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'express-async-errors';
|
||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
@ -16,7 +17,7 @@ const log = debug('snowball:server');
|
|||||||
const OAUTH_CLIENT_TYPE = 'oauth-app';
|
const OAUTH_CLIENT_TYPE = 'oauth-app';
|
||||||
|
|
||||||
export const main = async (): Promise<void> => {
|
export const main = async (): Promise<void> => {
|
||||||
const { server, database, gitHub, registryConfig, misc } = await getConfig();
|
const { server, database, gitHub, registryConfig } = await getConfig();
|
||||||
|
|
||||||
const app = new OAuthApp({
|
const app = new OAuthApp({
|
||||||
clientType: OAUTH_CLIENT_TYPE,
|
clientType: OAUTH_CLIENT_TYPE,
|
||||||
@ -24,7 +25,7 @@ export const main = async (): Promise<void> => {
|
|||||||
clientSecret: gitHub.oAuth.clientSecret,
|
clientSecret: gitHub.oAuth.clientSecret,
|
||||||
});
|
});
|
||||||
|
|
||||||
const db = new Database(database, misc);
|
const db = new Database(database);
|
||||||
await db.init();
|
await db.init();
|
||||||
|
|
||||||
const registry = new Registry(registryConfig);
|
const registry = new Registry(registryConfig);
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import debug from 'debug';
|
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { inc as semverInc } from 'semver';
|
import debug from 'debug';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { Octokit } from 'octokit';
|
||||||
|
import { inc as semverInc } from 'semver';
|
||||||
|
import { DeepPartial } from 'typeorm';
|
||||||
|
|
||||||
import { Registry as LaconicRegistry } from '@snowballtools/laconic-sdk';
|
import { Account, DEFAULT_GAS_ESTIMATION_MULTIPLIER, Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk';
|
||||||
|
import { DeliverTxResponse, IndexedTx } from '@cosmjs/stargate';
|
||||||
|
|
||||||
import { RegistryConfig } from './config';
|
import { RegistryConfig } from './config';
|
||||||
import {
|
import {
|
||||||
@ -12,49 +15,53 @@ import {
|
|||||||
ApplicationDeploymentRequest,
|
ApplicationDeploymentRequest,
|
||||||
ApplicationDeploymentRemovalRequest
|
ApplicationDeploymentRemovalRequest
|
||||||
} from './entity/Deployment';
|
} from './entity/Deployment';
|
||||||
import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types';
|
import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord } from './types';
|
||||||
import { sleep } from './utils';
|
import { getConfig, getRepoDetails, registryTransactionWithRetry, sleep } from './utils';
|
||||||
|
|
||||||
const log = debug('snowball:registry');
|
const log = debug('snowball:registry');
|
||||||
|
|
||||||
const APP_RECORD_TYPE = 'ApplicationRecord';
|
const APP_RECORD_TYPE = 'ApplicationRecord';
|
||||||
|
const APP_DEPLOYMENT_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction';
|
||||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
||||||
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
|
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
|
||||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
||||||
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
|
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
|
||||||
|
const WEBAPP_DEPLOYER_RECORD_TYPE = 'WebappDeployer'
|
||||||
const SLEEP_DURATION = 1000;
|
const SLEEP_DURATION = 1000;
|
||||||
|
|
||||||
// TODO: Move registry code to laconic-sdk/watcher-ts
|
// TODO: Move registry code to registry-sdk/watcher-ts
|
||||||
export class Registry {
|
export class Registry {
|
||||||
private registry: LaconicRegistry;
|
private registry: LaconicRegistry;
|
||||||
private registryConfig: RegistryConfig;
|
private registryConfig: RegistryConfig;
|
||||||
|
|
||||||
constructor (registryConfig: RegistryConfig) {
|
constructor(registryConfig: RegistryConfig) {
|
||||||
this.registryConfig = registryConfig;
|
this.registryConfig = registryConfig;
|
||||||
|
|
||||||
|
const gasPrice = getGasPrice(registryConfig.fee.gasPrice);
|
||||||
|
|
||||||
this.registry = new LaconicRegistry(
|
this.registry = new LaconicRegistry(
|
||||||
registryConfig.gqlEndpoint,
|
registryConfig.gqlEndpoint,
|
||||||
registryConfig.restEndpoint,
|
registryConfig.restEndpoint,
|
||||||
registryConfig.chainId
|
{ chainId: registryConfig.chainId, gasPrice }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationRecord ({
|
async createApplicationRecord({
|
||||||
appName,
|
octokit,
|
||||||
packageJSON,
|
repository,
|
||||||
commitHash,
|
commitHash,
|
||||||
appType,
|
appType,
|
||||||
repoUrl
|
|
||||||
}: {
|
}: {
|
||||||
appName: string;
|
octokit: Octokit
|
||||||
packageJSON: PackageJSON;
|
repository: string;
|
||||||
commitHash: string;
|
commitHash: string;
|
||||||
appType: string;
|
appType: string;
|
||||||
repoUrl: string;
|
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
applicationRecordId: string;
|
applicationRecordId: string;
|
||||||
applicationRecordData: ApplicationRecord;
|
applicationRecordData: ApplicationRecord;
|
||||||
}> {
|
}> {
|
||||||
// Use laconic-sdk to publish record
|
const { repo, repoUrl, packageJSON } = await getRepoDetails(octokit, repository, commitHash)
|
||||||
|
// Use registry-sdk to publish record
|
||||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||||
// Fetch previous records
|
// Fetch previous records
|
||||||
const records = await this.registry.queryRecords(
|
const records = await this.registry.queryRecords(
|
||||||
@ -87,7 +94,7 @@ export class Registry {
|
|||||||
repository_ref: commitHash,
|
repository_ref: commitHash,
|
||||||
repository: [repoUrl],
|
repository: [repoUrl],
|
||||||
app_type: appType,
|
app_type: appType,
|
||||||
name: appName,
|
name: repo,
|
||||||
...(packageJSON.description && { description: packageJSON.description }),
|
...(packageJSON.description && { description: packageJSON.description }),
|
||||||
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||||
...(packageJSON.license && { license: packageJSON.license }),
|
...(packageJSON.license && { license: packageJSON.license }),
|
||||||
@ -100,68 +107,158 @@ export class Registry {
|
|||||||
...(packageJSON.version && { app_version: packageJSON.version })
|
...(packageJSON.version && { app_version: packageJSON.version })
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this.registry.setRecord(
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
{
|
|
||||||
privateKey: this.registryConfig.privateKey,
|
const result = await registryTransactionWithRetry(() =>
|
||||||
record: applicationRecord,
|
this.registry.setRecord(
|
||||||
bondId: this.registryConfig.bondId
|
{
|
||||||
},
|
privateKey: this.registryConfig.privateKey,
|
||||||
'',
|
record: applicationRecord,
|
||||||
this.registryConfig.fee
|
bondId: this.registryConfig.bondId
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
log(`Published application record ${result.id}`);
|
||||||
log('Application record data:', applicationRecord);
|
log('Application record data:', applicationRecord);
|
||||||
|
|
||||||
// TODO: Discuss computation of CRN
|
// TODO: Discuss computation of LRN
|
||||||
const crn = this.getCrn(appName);
|
const lrn = this.getLrn(repo);
|
||||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
log(`Setting name: ${lrn} for record ID: ${result.id}`);
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
await sleep(SLEEP_DURATION);
|
||||||
await this.registry.setName(
|
await registryTransactionWithRetry(() =>
|
||||||
{ cid: result.data.id, crn },
|
this.registry.setName(
|
||||||
this.registryConfig.privateKey,
|
{
|
||||||
this.registryConfig.fee
|
cid: result.id,
|
||||||
|
lrn
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
await sleep(SLEEP_DURATION);
|
||||||
await this.registry.setName(
|
await registryTransactionWithRetry(() =>
|
||||||
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
this.registry.setName(
|
||||||
this.registryConfig.privateKey,
|
{
|
||||||
this.registryConfig.fee
|
cid: result.id,
|
||||||
|
lrn: `${lrn}@${applicationRecord.app_version}`
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
await sleep(SLEEP_DURATION);
|
||||||
await this.registry.setName(
|
await registryTransactionWithRetry(() =>
|
||||||
{
|
this.registry.setName(
|
||||||
cid: result.data.id,
|
{
|
||||||
crn: `${crn}@${applicationRecord.repository_ref}`
|
cid: result.id,
|
||||||
},
|
lrn: `${lrn}@${applicationRecord.repository_ref}`
|
||||||
this.registryConfig.privateKey,
|
},
|
||||||
this.registryConfig.fee
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
applicationRecordId: result.data.id,
|
applicationRecordId: result.id,
|
||||||
applicationRecordData: applicationRecord
|
applicationRecordData: applicationRecord
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationDeploymentRequest (data: {
|
async createApplicationDeploymentAuction(
|
||||||
|
appName: string,
|
||||||
|
octokit: Octokit,
|
||||||
|
auctionParams: AuctionParams,
|
||||||
|
data: DeepPartial<Deployment>,
|
||||||
|
): Promise<{
|
||||||
|
applicationDeploymentAuctionId: string;
|
||||||
|
}> {
|
||||||
|
assert(data.project?.repository, 'Project repository not found');
|
||||||
|
|
||||||
|
await this.createApplicationRecord({
|
||||||
|
octokit,
|
||||||
|
repository: data.project.repository,
|
||||||
|
appType: data.project!.template!,
|
||||||
|
commitHash: data.commitHash!,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lrn = this.getLrn(appName);
|
||||||
|
const config = await getConfig();
|
||||||
|
const auctionConfig = config.auction;
|
||||||
|
|
||||||
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
|
const auctionResult = await registryTransactionWithRetry(() =>
|
||||||
|
this.registry.createProviderAuction(
|
||||||
|
{
|
||||||
|
commitFee: auctionConfig.commitFee,
|
||||||
|
commitsDuration: auctionConfig.commitsDuration,
|
||||||
|
revealFee: auctionConfig.revealFee,
|
||||||
|
revealsDuration: auctionConfig.revealsDuration,
|
||||||
|
denom: auctionConfig.denom,
|
||||||
|
maxPrice: auctionParams.maxPrice,
|
||||||
|
numProviders: auctionParams.numProviders,
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!auctionResult.auction) {
|
||||||
|
throw new Error('Error creating auction');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create record of type applicationDeploymentAuction and publish
|
||||||
|
const applicationDeploymentAuction = {
|
||||||
|
application: lrn,
|
||||||
|
auction: auctionResult.auction.id,
|
||||||
|
type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await registryTransactionWithRetry(() =>
|
||||||
|
this.registry.setRecord(
|
||||||
|
{
|
||||||
|
privateKey: this.registryConfig.privateKey,
|
||||||
|
record: applicationDeploymentAuction,
|
||||||
|
bondId: this.registryConfig.bondId
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
log(`Application deployment auction created: ${auctionResult.auction.id}`);
|
||||||
|
log(`Application deployment auction record published: ${result.id}`);
|
||||||
|
log('Application deployment auction data:', applicationDeploymentAuction);
|
||||||
|
|
||||||
|
return {
|
||||||
|
applicationDeploymentAuctionId: auctionResult.auction.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createApplicationDeploymentRequest(data: {
|
||||||
deployment: Deployment,
|
deployment: Deployment,
|
||||||
appName: string,
|
appName: string,
|
||||||
repository: string,
|
repository: string,
|
||||||
|
auctionId?: string | null,
|
||||||
|
lrn: string,
|
||||||
environmentVariables: { [key: string]: string },
|
environmentVariables: { [key: string]: string },
|
||||||
dns: string,
|
dns: string,
|
||||||
|
payment?: string | null
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
applicationDeploymentRequestId: string;
|
applicationDeploymentRequestId: string;
|
||||||
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||||
}> {
|
}> {
|
||||||
const crn = this.getCrn(data.appName);
|
const lrn = this.getLrn(data.appName);
|
||||||
const records = await this.registry.resolveNames([crn]);
|
const records = await this.registry.resolveNames([lrn]);
|
||||||
const applicationRecord = records[0];
|
const applicationRecord = records[0];
|
||||||
|
|
||||||
if (!applicationRecord) {
|
if (!applicationRecord) {
|
||||||
throw new Error(`No record found for ${crn}`);
|
throw new Error(`No record found for ${lrn}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create record of type ApplicationDeploymentRequest and publish
|
// Create record of type ApplicationDeploymentRequest and publish
|
||||||
@ -169,12 +266,9 @@ export class Registry {
|
|||||||
type: APP_DEPLOYMENT_REQUEST_TYPE,
|
type: APP_DEPLOYMENT_REQUEST_TYPE,
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
||||||
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
application: `${lrn}@${applicationRecord.attributes.app_version}`,
|
||||||
dns: data.dns,
|
dns: data.dns,
|
||||||
|
|
||||||
// TODO: Not set in test-progressive-web-app CI
|
|
||||||
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
|
||||||
|
|
||||||
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
||||||
config: JSON.stringify({
|
config: JSON.stringify({
|
||||||
env: data.environmentVariables
|
env: data.environmentVariables
|
||||||
@ -185,32 +279,87 @@ export class Registry {
|
|||||||
)}`,
|
)}`,
|
||||||
repository: data.repository,
|
repository: data.repository,
|
||||||
repository_ref: data.deployment.commitHash
|
repository_ref: data.deployment.commitHash
|
||||||
})
|
}),
|
||||||
|
deployer: data.lrn,
|
||||||
|
...(data.auctionId && { auction: data.auctionId }),
|
||||||
|
...(data.payment && { payment: data.payment }),
|
||||||
};
|
};
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
await sleep(SLEEP_DURATION);
|
||||||
const result = await this.registry.setRecord(
|
|
||||||
{
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
privateKey: this.registryConfig.privateKey,
|
|
||||||
record: applicationDeploymentRequest,
|
const result = await registryTransactionWithRetry(() =>
|
||||||
bondId: this.registryConfig.bondId
|
this.registry.setRecord(
|
||||||
},
|
{
|
||||||
'',
|
privateKey: this.registryConfig.privateKey,
|
||||||
this.registryConfig.fee
|
record: applicationDeploymentRequest,
|
||||||
|
bondId: this.registryConfig.bondId
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
log(`Application deployment request record published: ${result.data.id}`);
|
|
||||||
|
log(`Application deployment request record published: ${result.id}`);
|
||||||
log('Application deployment request data:', applicationDeploymentRequest);
|
log('Application deployment request data:', applicationDeploymentRequest);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
applicationDeploymentRequestId: result.data.id,
|
applicationDeploymentRequestId: result.id,
|
||||||
applicationDeploymentRequestData: applicationDeploymentRequest
|
applicationDeploymentRequestData: applicationDeploymentRequest
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAuctionWinningDeployerRecords(
|
||||||
|
auctionId: string
|
||||||
|
): Promise<DeployerRecord[]> {
|
||||||
|
const records = await this.registry.getAuctionsByIds([auctionId]);
|
||||||
|
const auctionResult = records[0];
|
||||||
|
|
||||||
|
let deployerRecords = [];
|
||||||
|
const { winnerAddresses } = auctionResult;
|
||||||
|
|
||||||
|
for (const auctionWinner of winnerAddresses) {
|
||||||
|
const records = await this.getDeployerRecordsByFilter({
|
||||||
|
paymentAddress: auctionWinner,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newRecords = records.filter(record => {
|
||||||
|
return record.names !== null && record.names.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const record of newRecords) {
|
||||||
|
if (record.id) {
|
||||||
|
deployerRecords.push(record);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deployerRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseDeployerFunds(
|
||||||
|
auctionId: string
|
||||||
|
): Promise<any> {
|
||||||
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
|
const auction = await registryTransactionWithRetry(() =>
|
||||||
|
this.registry.releaseFunds(
|
||||||
|
{
|
||||||
|
auctionId
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return auction;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch ApplicationDeploymentRecords for deployments
|
* Fetch ApplicationDeploymentRecords for deployments
|
||||||
*/
|
*/
|
||||||
async getDeploymentRecords (
|
async getDeploymentRecords(
|
||||||
deployments: Deployment[]
|
deployments: Deployment[]
|
||||||
): Promise<AppDeploymentRecord[]> {
|
): Promise<AppDeploymentRecord[]> {
|
||||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
||||||
@ -222,20 +371,33 @@ export class Registry {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter records with ApplicationRecord ID and Deployment specific URL
|
// Filter records with ApplicationDeploymentRequestId ID and Deployment specific URL
|
||||||
return records.filter((record: AppDeploymentRecord) =>
|
return records.filter((record: AppDeploymentRecord) =>
|
||||||
deployments.some(
|
deployments.some(
|
||||||
(deployment) =>
|
(deployment) =>
|
||||||
deployment.applicationRecordId === record.attributes.application &&
|
deployment.applicationDeploymentRequestId === record.attributes.request &&
|
||||||
record.attributes.url.includes(deployment.id)
|
record.attributes.url.includes(deployment.id)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch WebappDeployer Records by filter
|
||||||
|
*/
|
||||||
|
async getDeployerRecordsByFilter(filter: { [key: string]: any }): Promise<DeployerRecord[]> {
|
||||||
|
return this.registry.queryRecords(
|
||||||
|
{
|
||||||
|
type: WEBAPP_DEPLOYER_RECORD_TYPE,
|
||||||
|
...filter
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch ApplicationDeploymentRecords by filter
|
* Fetch ApplicationDeploymentRecords by filter
|
||||||
*/
|
*/
|
||||||
async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
async getDeploymentRecordsByFilter(filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
||||||
return this.registry.queryRecords(
|
return this.registry.queryRecords(
|
||||||
{
|
{
|
||||||
type: APP_DEPLOYMENT_RECORD_TYPE,
|
type: APP_DEPLOYMENT_RECORD_TYPE,
|
||||||
@ -248,7 +410,7 @@ export class Registry {
|
|||||||
/**
|
/**
|
||||||
* Fetch ApplicationDeploymentRemovalRecords for deployments
|
* Fetch ApplicationDeploymentRemovalRecords for deployments
|
||||||
*/
|
*/
|
||||||
async getDeploymentRemovalRecords (
|
async getDeploymentRemovalRecords(
|
||||||
deployments: Deployment[]
|
deployments: Deployment[]
|
||||||
): Promise<AppDeploymentRemovalRecord[]> {
|
): Promise<AppDeploymentRemovalRecord[]> {
|
||||||
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
|
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
|
||||||
@ -269,8 +431,11 @@ export class Registry {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationDeploymentRemovalRequest (data: {
|
async createApplicationDeploymentRemovalRequest(data: {
|
||||||
deploymentId: string;
|
deploymentId: string;
|
||||||
|
deployerLrn: string;
|
||||||
|
auctionId?: string | null;
|
||||||
|
payment?: string | null;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
applicationDeploymentRemovalRequestId: string;
|
applicationDeploymentRemovalRequestId: string;
|
||||||
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
||||||
@ -278,30 +443,93 @@ export class Registry {
|
|||||||
const applicationDeploymentRemovalRequest = {
|
const applicationDeploymentRemovalRequest = {
|
||||||
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
|
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
deployment: data.deploymentId
|
deployment: data.deploymentId,
|
||||||
|
deployer: data.deployerLrn,
|
||||||
|
...(data.auctionId && { auction: data.auctionId }),
|
||||||
|
...(data.payment && { payment: data.payment }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this.registry.setRecord(
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
{
|
|
||||||
privateKey: this.registryConfig.privateKey,
|
const result = await registryTransactionWithRetry(() =>
|
||||||
record: applicationDeploymentRemovalRequest,
|
this.registry.setRecord(
|
||||||
bondId: this.registryConfig.bondId
|
{
|
||||||
},
|
privateKey: this.registryConfig.privateKey,
|
||||||
'',
|
record: applicationDeploymentRemovalRequest,
|
||||||
this.registryConfig.fee
|
bondId: this.registryConfig.bondId
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
fee
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
log(`Application deployment removal request record published: ${result.data.id}`);
|
log(`Application deployment removal request record published: ${result.id}`);
|
||||||
log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
|
log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
applicationDeploymentRemovalRequestId: result.data.id,
|
applicationDeploymentRemovalRequestId: result.id,
|
||||||
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
|
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getCrn (appName: string): string {
|
async getCompletedAuctionIds(auctionIds: string[]): Promise<string[]> {
|
||||||
|
if (auctionIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const auctions = await this.registry.getAuctionsByIds(auctionIds);
|
||||||
|
|
||||||
|
const completedAuctions = auctions
|
||||||
|
.filter((auction: { id: string, status: string }) => auction.status === 'completed')
|
||||||
|
.map((auction: { id: string, status: string }) => auction.id);
|
||||||
|
|
||||||
|
return completedAuctions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRecordsByName(name: string): Promise<any> {
|
||||||
|
return this.registry.resolveNames([name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuctionData(auctionId: string): Promise<any> {
|
||||||
|
return this.registry.getAuctionsByIds([auctionId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTokensToAccount(receiverAddress: string, amount: string): Promise<DeliverTxResponse> {
|
||||||
|
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||||
|
const account = await this.getAccount();
|
||||||
|
const laconicClient = await this.registry.getLaconicClient(account);
|
||||||
|
const txResponse: DeliverTxResponse =
|
||||||
|
await registryTransactionWithRetry(() =>
|
||||||
|
laconicClient.sendTokens(account.address, receiverAddress,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
denom: 'alnt',
|
||||||
|
amount
|
||||||
|
}
|
||||||
|
],
|
||||||
|
fee || DEFAULT_GAS_ESTIMATION_MULTIPLIER)
|
||||||
|
);
|
||||||
|
|
||||||
|
return txResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccount(): Promise<Account> {
|
||||||
|
const account = new Account(Buffer.from(this.registryConfig.privateKey, 'hex'));
|
||||||
|
await account.init();
|
||||||
|
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTxResponse(txHash: string): Promise<IndexedTx | null> {
|
||||||
|
const account = await this.getAccount();
|
||||||
|
const laconicClient = await this.registry.getLaconicClient(account);
|
||||||
|
const txResponse: IndexedTx | null = await laconicClient.getTx(txHash);
|
||||||
|
|
||||||
|
return txResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLrn(appName: string): string {
|
||||||
assert(this.registryConfig.authority, "Authority doesn't exist");
|
assert(this.registryConfig.authority, "Authority doesn't exist");
|
||||||
return `crn://${this.registryConfig.authority}/applications/${appName}`;
|
return `lrn://${this.registryConfig.authority}/applications/${appName}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { Permission } from './entity/ProjectMember';
|
|||||||
import { Domain } from './entity/Domain';
|
import { Domain } from './entity/Domain';
|
||||||
import { Project } from './entity/Project';
|
import { Project } from './entity/Project';
|
||||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||||
|
import { AddProjectFromTemplateInput, AuctionParams, EnvironmentVariables } from './types';
|
||||||
|
|
||||||
const log = debug('snowball:resolver');
|
const log = debug('snowball:resolver');
|
||||||
|
|
||||||
@ -17,16 +18,23 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
return context.user;
|
return context.user;
|
||||||
},
|
},
|
||||||
|
|
||||||
organizations: async (_:any, __: any, context: any) => {
|
organizations: async (_: any, __: any, context: any) => {
|
||||||
return service.getOrganizationsByUserId(context.user);
|
return service.getOrganizationsByUserId(context.user);
|
||||||
},
|
},
|
||||||
|
|
||||||
project: async (_: any, { projectId }: { projectId: string }) => {
|
project: async (_: any, { projectId }: { projectId: string }, context: any) => {
|
||||||
return service.getProjectById(projectId);
|
return service.getProjectById(context.user, projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
|
projectsInOrganization: async (
|
||||||
return service.getProjectsInOrganization(context.user, organizationSlug);
|
_: any,
|
||||||
|
{ organizationSlug }: { organizationSlug: string },
|
||||||
|
context: any,
|
||||||
|
) => {
|
||||||
|
return service.getProjectsInOrganization(
|
||||||
|
context.user,
|
||||||
|
organizationSlug,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
@ -35,7 +43,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
|
|
||||||
environmentVariables: async (
|
environmentVariables: async (
|
||||||
_: any,
|
_: any,
|
||||||
{ projectId }: { projectId: string }
|
{ projectId }: { projectId: string },
|
||||||
) => {
|
) => {
|
||||||
return service.getEnvironmentVariablesByProjectId(projectId);
|
return service.getEnvironmentVariablesByProjectId(projectId);
|
||||||
},
|
},
|
||||||
@ -44,7 +52,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
return service.getProjectMembersByProjectId(projectId);
|
return service.getProjectMembersByProjectId(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
|
searchProjects: async (
|
||||||
|
_: any,
|
||||||
|
{ searchText }: { searchText: string },
|
||||||
|
context: any,
|
||||||
|
) => {
|
||||||
return service.searchProjects(context.user, searchText);
|
return service.searchProjects(context.user, searchText);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -52,11 +64,37 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
_: any,
|
_: any,
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
filter
|
filter,
|
||||||
}: { projectId: string; filter?: FindOptionsWhere<Domain> }
|
}: { projectId: string; filter?: FindOptionsWhere<Domain> },
|
||||||
) => {
|
) => {
|
||||||
return service.getDomainsByProjectId(projectId, filter);
|
return service.getDomainsByProjectId(projectId, filter);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
getAuctionData: async (
|
||||||
|
_: any,
|
||||||
|
{ auctionId }: { auctionId: string },
|
||||||
|
) => {
|
||||||
|
return service.getAuctionData(auctionId);
|
||||||
|
},
|
||||||
|
|
||||||
|
deployers: async (_: any, __: any, context: any) => {
|
||||||
|
return service.getDeployers();
|
||||||
|
},
|
||||||
|
|
||||||
|
address: async (_: any, __: any, context: any) => {
|
||||||
|
return service.getAddress();
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyTx: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
txHash,
|
||||||
|
amount,
|
||||||
|
senderAddress,
|
||||||
|
}: { txHash: string; amount: string; senderAddress: string },
|
||||||
|
) => {
|
||||||
|
return service.verifyTx(txHash, amount, senderAddress);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Return error in GQL response
|
// TODO: Return error in GQL response
|
||||||
@ -64,10 +102,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
removeProjectMember: async (
|
removeProjectMember: async (
|
||||||
_: any,
|
_: any,
|
||||||
{ projectMemberId }: { projectMemberId: string },
|
{ projectMemberId }: { projectMemberId: string },
|
||||||
context: any
|
context: any,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.removeProjectMember(context.user, projectMemberId);
|
return await service.removeProjectMember(
|
||||||
|
context.user,
|
||||||
|
projectMemberId,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@ -78,13 +119,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
_: any,
|
_: any,
|
||||||
{
|
{
|
||||||
projectMemberId,
|
projectMemberId,
|
||||||
data
|
data,
|
||||||
}: {
|
}: {
|
||||||
projectMemberId: string;
|
projectMemberId: string;
|
||||||
data: {
|
data: {
|
||||||
permissions: Permission[];
|
permissions: Permission[];
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateProjectMember(projectMemberId, data);
|
return await service.updateProjectMember(projectMemberId, data);
|
||||||
@ -98,14 +139,14 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
_: any,
|
_: any,
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
data
|
data,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
data: {
|
data: {
|
||||||
email: string;
|
email: string;
|
||||||
permissions: Permission[];
|
permissions: Permission[];
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.addProjectMember(projectId, data));
|
return Boolean(await service.addProjectMember(projectId, data));
|
||||||
@ -119,15 +160,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
_: any,
|
_: any,
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
data
|
data,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
data: { environments: string[]; key: string; value: string }[];
|
data: { environments: string[]; key: string; value: string }[];
|
||||||
}
|
},
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
await service.addEnvironmentVariables(projectId, data)
|
await service.addEnvironmentVariables(projectId, data),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
@ -139,16 +180,16 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
_: any,
|
_: any,
|
||||||
{
|
{
|
||||||
environmentVariableId,
|
environmentVariableId,
|
||||||
data
|
data,
|
||||||
}: {
|
}: {
|
||||||
environmentVariableId: string;
|
environmentVariableId: string;
|
||||||
data: DeepPartial<EnvironmentVariable>;
|
data: DeepPartial<EnvironmentVariable>;
|
||||||
}
|
},
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateEnvironmentVariable(
|
return await service.updateEnvironmentVariable(
|
||||||
environmentVariableId,
|
environmentVariableId,
|
||||||
data
|
data,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
@ -158,7 +199,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
|
|
||||||
removeEnvironmentVariable: async (
|
removeEnvironmentVariable: async (
|
||||||
_: any,
|
_: any,
|
||||||
{ environmentVariableId }: { environmentVariableId: string }
|
{ environmentVariableId }: { environmentVariableId: string },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.removeEnvironmentVariable(environmentVariableId);
|
return await service.removeEnvironmentVariable(environmentVariableId);
|
||||||
@ -171,26 +212,76 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
updateDeploymentToProd: async (
|
updateDeploymentToProd: async (
|
||||||
_: any,
|
_: any,
|
||||||
{ deploymentId }: { deploymentId: string },
|
{ deploymentId }: { deploymentId: string },
|
||||||
context: any
|
context: any,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
|
return Boolean(
|
||||||
|
await service.updateDeploymentToProd(context.user, deploymentId),
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addProjectFromTemplate: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
organizationSlug,
|
||||||
|
data,
|
||||||
|
lrn,
|
||||||
|
auctionParams,
|
||||||
|
environmentVariables
|
||||||
|
}: {
|
||||||
|
organizationSlug: string;
|
||||||
|
data: AddProjectFromTemplateInput;
|
||||||
|
lrn: string;
|
||||||
|
auctionParams: AuctionParams;
|
||||||
|
environmentVariables: EnvironmentVariables[];
|
||||||
|
},
|
||||||
|
context: any,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await service.addProjectFromTemplate(
|
||||||
|
context.user,
|
||||||
|
organizationSlug,
|
||||||
|
data,
|
||||||
|
lrn,
|
||||||
|
auctionParams,
|
||||||
|
environmentVariables
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
log(err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
addProject: async (
|
addProject: async (
|
||||||
_: any,
|
_: any,
|
||||||
{
|
{
|
||||||
organizationSlug,
|
organizationSlug,
|
||||||
data
|
data,
|
||||||
}: { organizationSlug: string; data: DeepPartial<Project> },
|
lrn,
|
||||||
context: any
|
auctionParams,
|
||||||
|
environmentVariables
|
||||||
|
}: {
|
||||||
|
organizationSlug: string;
|
||||||
|
data: DeepPartial<Project>;
|
||||||
|
lrn: string;
|
||||||
|
auctionParams: AuctionParams,
|
||||||
|
environmentVariables: EnvironmentVariables[];
|
||||||
|
},
|
||||||
|
context: any,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.addProject(context.user, organizationSlug, data);
|
return await service.addProject(
|
||||||
|
context.user,
|
||||||
|
organizationSlug,
|
||||||
|
data,
|
||||||
|
lrn,
|
||||||
|
auctionParams,
|
||||||
|
environmentVariables
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
throw err;
|
throw err;
|
||||||
@ -199,7 +290,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
|
|
||||||
updateProject: async (
|
updateProject: async (
|
||||||
_: any,
|
_: any,
|
||||||
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }
|
{ projectId, data }: { projectId: string; data: DeepPartial<Project> },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateProject(projectId, data);
|
return await service.updateProject(projectId, data);
|
||||||
@ -212,10 +303,12 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
redeployToProd: async (
|
redeployToProd: async (
|
||||||
_: any,
|
_: any,
|
||||||
{ deploymentId }: { deploymentId: string },
|
{ deploymentId }: { deploymentId: string },
|
||||||
context: any
|
context: any,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.redeployToProd(context.user, deploymentId));
|
return Boolean(
|
||||||
|
await service.redeployToProd(context.user, deploymentId),
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@ -244,8 +337,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
_: any,
|
_: any,
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
deploymentId
|
deploymentId,
|
||||||
}: { deploymentId: string; projectId: string }
|
}: { deploymentId: string; projectId: string },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.rollbackDeployment(projectId, deploymentId);
|
return await service.rollbackDeployment(projectId, deploymentId);
|
||||||
@ -257,9 +350,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
|
|
||||||
deleteDeployment: async (
|
deleteDeployment: async (
|
||||||
_: any,
|
_: any,
|
||||||
{
|
{ deploymentId }: { deploymentId: string },
|
||||||
deploymentId
|
|
||||||
}: { deploymentId: string; }
|
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.deleteDeployment(deploymentId);
|
return await service.deleteDeployment(deploymentId);
|
||||||
@ -271,7 +362,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
|
|
||||||
addDomain: async (
|
addDomain: async (
|
||||||
_: any,
|
_: any,
|
||||||
{ projectId, data }: { projectId: string; data: { name: string } }
|
{ projectId, data }: { projectId: string; data: { name: string } },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.addDomain(projectId, data));
|
return Boolean(await service.addDomain(projectId, data));
|
||||||
@ -283,7 +374,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
|
|
||||||
updateDomain: async (
|
updateDomain: async (
|
||||||
_: any,
|
_: any,
|
||||||
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> }
|
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateDomain(domainId, data);
|
return await service.updateDomain(domainId, data);
|
||||||
@ -296,7 +387,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
authenticateGitHub: async (
|
authenticateGitHub: async (
|
||||||
_: any,
|
_: any,
|
||||||
{ code }: { code: string },
|
{ code }: { code: string },
|
||||||
context: any
|
context: any,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.authenticateGitHub(code, context.user);
|
return await service.authenticateGitHub(code, context.user);
|
||||||
@ -308,12 +399,14 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
|
|
||||||
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
unauthenticateGitHub: async (_: any, __: object, context: any) => {
|
||||||
try {
|
try {
|
||||||
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
|
return service.unauthenticateGitHub(context.user, {
|
||||||
|
gitHubToken: null,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -19,6 +19,7 @@ router.get('/registration/:email', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post('/register', async (req, res) => {
|
router.post('/register', async (req, res) => {
|
||||||
|
console.log('Register', req.body);
|
||||||
const { email, challenge, attestation } = req.body;
|
const { email, challenge, attestation } = req.body;
|
||||||
const user = await createUser(req.app.get('service'), {
|
const user = await createUser(req.app.get('service'), {
|
||||||
challenge,
|
challenge,
|
||||||
@ -26,18 +27,19 @@ router.post('/register', async (req, res) => {
|
|||||||
userEmail: email,
|
userEmail: email,
|
||||||
userName: email.split('@')[0],
|
userName: email.split('@')[0],
|
||||||
});
|
});
|
||||||
req.session.userId = user.id;
|
req.session.address = user.id;
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/authenticate', async (req, res) => {
|
router.post('/authenticate', async (req, res) => {
|
||||||
|
console.log('Authenticate', req.body);
|
||||||
const { signedWhoamiRequest } = req.body;
|
const { signedWhoamiRequest } = req.body;
|
||||||
const user = await authenticateUser(
|
const user = await authenticateUser(
|
||||||
req.app.get('service'),
|
req.app.get('service'),
|
||||||
signedWhoamiRequest,
|
signedWhoamiRequest,
|
||||||
);
|
);
|
||||||
if (user) {
|
if (user) {
|
||||||
req.session.userId = user.id;
|
req.session.address = user.id;
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
} else {
|
} else {
|
||||||
res.sendStatus(401);
|
res.sendStatus(401);
|
||||||
@ -45,11 +47,10 @@ router.post('/authenticate', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//
|
//
|
||||||
// Lit
|
// SIWE Auth
|
||||||
//
|
//
|
||||||
|
|
||||||
router.post('/validate', async (req, res) => {
|
router.post('/validate', async (req, res) => {
|
||||||
const { message, signature, action } = req.body;
|
const { message, signature } = req.body;
|
||||||
const { success, data } = await new SiweMessage(message).verify({
|
const { success, data } = await new SiweMessage(message).verify({
|
||||||
signature,
|
signature,
|
||||||
});
|
});
|
||||||
@ -60,23 +61,20 @@ router.post('/validate', async (req, res) => {
|
|||||||
const service: Service = req.app.get('service');
|
const service: Service = req.app.get('service');
|
||||||
const user = await service.getUserByEthAddress(data.address);
|
const user = await service.getUserByEthAddress(data.address);
|
||||||
|
|
||||||
if (action === 'signup') {
|
if (!user) {
|
||||||
if (user) {
|
|
||||||
return res.send({ success: false, error: 'user_already_exists' });
|
|
||||||
}
|
|
||||||
const newUser = await service.createUser({
|
const newUser = await service.createUser({
|
||||||
ethAddress: data.address,
|
ethAddress: data.address,
|
||||||
email: '',
|
email: `${data.address}@example.com`,
|
||||||
name: '',
|
|
||||||
subOrgId: '',
|
subOrgId: '',
|
||||||
turnkeyWalletId: '',
|
turnkeyWalletId: '',
|
||||||
});
|
});
|
||||||
req.session.userId = newUser.id;
|
|
||||||
} else if (action === 'login') {
|
// SIWESession from the web3modal library requires both address and chain ID
|
||||||
if (!user) {
|
req.session.address = newUser.id;
|
||||||
return res.send({ success: false, error: 'user_not_found' });
|
req.session.chainId = data.chainId;
|
||||||
}
|
} else {
|
||||||
req.session.userId = user.id;
|
req.session.address = user.id;
|
||||||
|
req.session.chainId = data.chainId;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send({ success });
|
res.send({ success });
|
||||||
@ -86,9 +84,10 @@ router.post('/validate', async (req, res) => {
|
|||||||
// General
|
// General
|
||||||
//
|
//
|
||||||
router.get('/session', (req, res) => {
|
router.get('/session', (req, res) => {
|
||||||
if (req.session.userId) {
|
if (req.session.address && req.session.chainId) {
|
||||||
res.send({
|
res.send({
|
||||||
userId: req.session.userId,
|
address: req.session.address,
|
||||||
|
chainId: req.session.chainId
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(401).send({ error: 'Unauthorized: No active session' });
|
res.status(401).send({ error: 'Unauthorized: No active session' });
|
||||||
@ -96,9 +95,12 @@ router.get('/session', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post('/logout', (req, res) => {
|
router.post('/logout', (req, res) => {
|
||||||
// This is how you clear cookie-session
|
req.session.destroy((err) => {
|
||||||
(req as any).session = null;
|
if (err) {
|
||||||
res.send({ success: true });
|
return res.send({ success: false });
|
||||||
|
}
|
||||||
|
res.send({ success: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
9
packages/backend/src/routes/staging.ts
Normal file
9
packages/backend/src/routes/staging.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/version', async (req, res) => {
|
||||||
|
return res.send({ version: '0.0.9' });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
@ -22,6 +22,13 @@ enum DeploymentStatus {
|
|||||||
Deleting
|
Deleting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AuctionStatus {
|
||||||
|
completed
|
||||||
|
reveal
|
||||||
|
commit
|
||||||
|
expired
|
||||||
|
}
|
||||||
|
|
||||||
enum DomainStatus {
|
enum DomainStatus {
|
||||||
Live
|
Live
|
||||||
Pending
|
Pending
|
||||||
@ -65,8 +72,13 @@ type Project {
|
|||||||
repository: String!
|
repository: String!
|
||||||
prodBranch: String!
|
prodBranch: String!
|
||||||
description: String
|
description: String
|
||||||
|
deployers: [Deployer!]
|
||||||
|
auctionId: String
|
||||||
|
fundsReleased: Boolean
|
||||||
template: String
|
template: String
|
||||||
framework: String
|
framework: String
|
||||||
|
paymentAddress: String!
|
||||||
|
txHash: String!
|
||||||
webhooks: [String!]
|
webhooks: [String!]
|
||||||
members: [ProjectMember!]
|
members: [ProjectMember!]
|
||||||
environmentVariables: [EnvironmentVariable!]
|
environmentVariables: [EnvironmentVariable!]
|
||||||
@ -74,7 +86,7 @@ type Project {
|
|||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
organization: Organization!
|
organization: Organization!
|
||||||
icon: String
|
icon: String
|
||||||
subDomain: String
|
baseDomains: [String!]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectMember {
|
type ProjectMember {
|
||||||
@ -94,7 +106,10 @@ type Deployment {
|
|||||||
commitMessage: String!
|
commitMessage: String!
|
||||||
url: String
|
url: String
|
||||||
environment: Environment!
|
environment: Environment!
|
||||||
|
deployer: Deployer
|
||||||
|
applicationDeploymentRequestId: String
|
||||||
isCurrent: Boolean!
|
isCurrent: Boolean!
|
||||||
|
baseDomain: String
|
||||||
status: DeploymentStatus!
|
status: DeploymentStatus!
|
||||||
createdAt: String!
|
createdAt: String!
|
||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
@ -120,6 +135,17 @@ type EnvironmentVariable {
|
|||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Deployer {
|
||||||
|
deployerLrn: String!
|
||||||
|
deployerId: String!
|
||||||
|
deployerApiUrl: String!
|
||||||
|
minimumPayment: String
|
||||||
|
paymentAddress: String
|
||||||
|
createdAt: String!
|
||||||
|
updatedAt: String!
|
||||||
|
baseDomain: String
|
||||||
|
}
|
||||||
|
|
||||||
type AuthResult {
|
type AuthResult {
|
||||||
token: String!
|
token: String!
|
||||||
}
|
}
|
||||||
@ -130,11 +156,23 @@ input AddEnvironmentVariableInput {
|
|||||||
value: String!
|
value: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input AddProjectFromTemplateInput {
|
||||||
|
templateOwner: String!
|
||||||
|
templateRepo: String!
|
||||||
|
owner: String!
|
||||||
|
name: String!
|
||||||
|
isPrivate: Boolean!
|
||||||
|
paymentAddress: String!
|
||||||
|
txHash: String!
|
||||||
|
}
|
||||||
|
|
||||||
input AddProjectInput {
|
input AddProjectInput {
|
||||||
name: String!
|
name: String!
|
||||||
repository: String!
|
repository: String!
|
||||||
prodBranch: String!
|
prodBranch: String!
|
||||||
template: String
|
template: String
|
||||||
|
paymentAddress: String!
|
||||||
|
txHash: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
input UpdateProjectInput {
|
input UpdateProjectInput {
|
||||||
@ -174,6 +212,48 @@ input FilterDomainsInput {
|
|||||||
status: DomainStatus
|
status: DomainStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Fee {
|
||||||
|
type: String!
|
||||||
|
quantity: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bid {
|
||||||
|
auctionId: String!
|
||||||
|
bidderAddress: String!
|
||||||
|
status: String!
|
||||||
|
commitHash: String!
|
||||||
|
commitTime: String
|
||||||
|
commitFee: Fee
|
||||||
|
revealTime: String
|
||||||
|
revealFee: Fee
|
||||||
|
bidAmount: Fee
|
||||||
|
}
|
||||||
|
|
||||||
|
type Auction {
|
||||||
|
id: String!
|
||||||
|
kind: String!
|
||||||
|
status: String!
|
||||||
|
ownerAddress: String!
|
||||||
|
createTime: String!
|
||||||
|
commitsEndTime: String!
|
||||||
|
revealsEndTime: String!
|
||||||
|
commitFee: Fee!
|
||||||
|
revealFee: Fee!
|
||||||
|
minimumBid: Fee
|
||||||
|
winnerAddresses: [String!]!
|
||||||
|
winnerBids: [Fee!]
|
||||||
|
winnerPrice: Fee
|
||||||
|
maxPrice: Fee
|
||||||
|
numProviders: Int!
|
||||||
|
fundsReleased: Boolean!
|
||||||
|
bids: [Bid!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
input AuctionParams {
|
||||||
|
maxPrice: String,
|
||||||
|
numProviders: Int,
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
user: User!
|
user: User!
|
||||||
organizations: [Organization!]
|
organizations: [Organization!]
|
||||||
@ -184,7 +264,11 @@ type Query {
|
|||||||
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
||||||
projectMembers(projectId: String!): [ProjectMember!]
|
projectMembers(projectId: String!): [ProjectMember!]
|
||||||
searchProjects(searchText: String!): [Project!]
|
searchProjects(searchText: String!): [Project!]
|
||||||
|
getAuctionData(auctionId: String!): Auction!
|
||||||
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
||||||
|
deployers: [Deployer]
|
||||||
|
address: String!
|
||||||
|
verifyTx(txHash: String!, amount: String!, senderAddress: String!): Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
@ -204,7 +288,20 @@ type Mutation {
|
|||||||
): Boolean!
|
): Boolean!
|
||||||
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
||||||
updateDeploymentToProd(deploymentId: String!): Boolean!
|
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||||
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
addProjectFromTemplate(
|
||||||
|
organizationSlug: String!
|
||||||
|
data: AddProjectFromTemplateInput
|
||||||
|
lrn: String
|
||||||
|
auctionParams: AuctionParams
|
||||||
|
environmentVariables: [AddEnvironmentVariableInput!]
|
||||||
|
): Project!
|
||||||
|
addProject(
|
||||||
|
organizationSlug: String!
|
||||||
|
data: AddProjectInput!
|
||||||
|
lrn: String
|
||||||
|
auctionParams: AuctionParams
|
||||||
|
environmentVariables: [AddEnvironmentVariableInput!]
|
||||||
|
): Project!
|
||||||
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
|
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
|
||||||
redeployToProd(deploymentId: String!): Boolean!
|
redeployToProd(deploymentId: String!): Boolean!
|
||||||
deleteProject(projectId: String!): Boolean!
|
deleteProject(projectId: String!): Boolean!
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
ApolloServerPluginLandingPageLocalDefault,
|
ApolloServerPluginLandingPageLocalDefault,
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
} from 'apollo-server-core';
|
} from 'apollo-server-core';
|
||||||
import cookieSession from 'cookie-session';
|
import session from 'express-session';
|
||||||
|
|
||||||
import { TypeSource } from '@graphql-tools/utils';
|
import { TypeSource } from '@graphql-tools/utils';
|
||||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||||
@ -17,13 +17,18 @@ import { ServerConfig } from './config';
|
|||||||
import { DEFAULT_GQL_PATH } from './constants';
|
import { DEFAULT_GQL_PATH } from './constants';
|
||||||
import githubRouter from './routes/github';
|
import githubRouter from './routes/github';
|
||||||
import authRouter from './routes/auth';
|
import authRouter from './routes/auth';
|
||||||
|
import stagingRouter from './routes/staging';
|
||||||
import { Service } from './service';
|
import { Service } from './service';
|
||||||
|
|
||||||
const log = debug('snowball:server');
|
const log = debug('snowball:server');
|
||||||
|
|
||||||
|
// Set cookie expiration to 1 month in milliseconds
|
||||||
|
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
declare module 'express-session' {
|
declare module 'express-session' {
|
||||||
interface SessionData {
|
interface SessionData {
|
||||||
userId: string;
|
address: string;
|
||||||
|
chainId: number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,14 +58,13 @@ export const createAndStartServer = async (
|
|||||||
context: async ({ req }) => {
|
context: async ({ req }) => {
|
||||||
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
||||||
|
|
||||||
const { userId } = req.session;
|
const { address } = req.session;
|
||||||
|
|
||||||
if (!userId) {
|
if (!address) {
|
||||||
throw new AuthenticationError('Unauthorized: No active session');
|
throw new AuthenticationError('Unauthorized: No active session');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await service.getUser(userId);
|
const user = await service.getUser(address);
|
||||||
|
|
||||||
return { user };
|
return { user };
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
@ -79,20 +83,25 @@ export const createAndStartServer = async (
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sessionOptions: session.SessionOptions = {
|
||||||
|
secret: secret,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: true,
|
||||||
|
cookie: {
|
||||||
|
secure: new URL(appOriginUrl).protocol === 'https:',
|
||||||
|
maxAge: COOKIE_MAX_AGE,
|
||||||
|
domain: domain || undefined,
|
||||||
|
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (trustProxy) {
|
if (trustProxy) {
|
||||||
// trust first proxy
|
// trust first proxy
|
||||||
app.set('trust proxy', 1);
|
app.set('trust proxy', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
cookieSession({
|
session(sessionOptions)
|
||||||
secret: secret,
|
|
||||||
secure: new URL(appOriginUrl).protocol === 'https:',
|
|
||||||
// 23 hours (less than 24 hours to avoid sessionSigs expiration issues)
|
|
||||||
maxAge: 23 * 60 * 60 * 1000,
|
|
||||||
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
|
|
||||||
domain: domain || undefined,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
server.applyMiddleware({
|
server.applyMiddleware({
|
||||||
@ -109,6 +118,12 @@ export const createAndStartServer = async (
|
|||||||
app.set('service', service);
|
app.set('service', service);
|
||||||
app.use('/auth', authRouter);
|
app.use('/auth', authRouter);
|
||||||
app.use('/api/github', githubRouter);
|
app.use('/api/github', githubRouter);
|
||||||
|
app.use('/staging', stagingRouter);
|
||||||
|
|
||||||
|
app.use((err: any, req: any, res: any, next: any) => {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
});
|
||||||
|
|
||||||
httpServer.listen(port, host, () => {
|
httpServer.listen(port, host, () => {
|
||||||
log(`Server is listening on ${host}:${port}${server.graphqlPath}`);
|
log(`Server is listening on ${host}:${port}${server.graphqlPath}`);
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -29,6 +29,8 @@ export interface GitPushEventPayload {
|
|||||||
|
|
||||||
export interface AppDeploymentRecordAttributes {
|
export interface AppDeploymentRecordAttributes {
|
||||||
application: string;
|
application: string;
|
||||||
|
auction: string;
|
||||||
|
deployer: string;
|
||||||
dns: string;
|
dns: string;
|
||||||
meta: string;
|
meta: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -41,7 +43,7 @@ export interface AppDeploymentRecordAttributes {
|
|||||||
export interface AppDeploymentRemovalRecordAttributes {
|
export interface AppDeploymentRemovalRecordAttributes {
|
||||||
deployment: string;
|
deployment: string;
|
||||||
request: string;
|
request: string;
|
||||||
type: "ApplicationDeploymentRemovalRecord";
|
type: 'ApplicationDeploymentRemovalRecord';
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,3 +63,42 @@ export interface AppDeploymentRecord extends RegistryRecord {
|
|||||||
export interface AppDeploymentRemovalRecord extends RegistryRecord {
|
export interface AppDeploymentRemovalRecord extends RegistryRecord {
|
||||||
attributes: AppDeploymentRemovalRecordAttributes;
|
attributes: AppDeploymentRemovalRecordAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AddProjectFromTemplateInput {
|
||||||
|
templateOwner: string;
|
||||||
|
templateRepo: string;
|
||||||
|
owner: string;
|
||||||
|
name: string;
|
||||||
|
isPrivate: boolean;
|
||||||
|
paymentAddress: string;
|
||||||
|
txHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuctionParams {
|
||||||
|
maxPrice: string,
|
||||||
|
numProviders: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnvironmentVariables {
|
||||||
|
environments: string[],
|
||||||
|
key: string,
|
||||||
|
value: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeployerRecord {
|
||||||
|
id: string;
|
||||||
|
names: string[];
|
||||||
|
owners: string[];
|
||||||
|
bondId: string;
|
||||||
|
createTime: string;
|
||||||
|
expiryTime: string;
|
||||||
|
attributes: {
|
||||||
|
apiUrl: string;
|
||||||
|
minimumPayment: string | null;
|
||||||
|
name: string;
|
||||||
|
paymentAddress: string;
|
||||||
|
publicKey: string;
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import debug from 'debug';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
import { Octokit } from 'octokit';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import toml from 'toml';
|
import toml from 'toml';
|
||||||
import debug from 'debug';
|
|
||||||
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||||
|
|
||||||
import { Config } from './config';
|
import { Config } from './config';
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
||||||
|
import { PackageJSON } from './types';
|
||||||
|
|
||||||
const log = debug('snowball:utils');
|
const log = debug('snowball:utils');
|
||||||
|
|
||||||
@ -76,3 +80,64 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
|||||||
|
|
||||||
export const sleep = async (ms: number): Promise<void> =>
|
export const sleep = async (ms: number): Promise<void> =>
|
||||||
new Promise((resolve) => setTimeout(resolve, ms));
|
new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
export const getRepoDetails = async (
|
||||||
|
octokit: Octokit,
|
||||||
|
repository: string,
|
||||||
|
commitHash: string | undefined,
|
||||||
|
): Promise<{
|
||||||
|
repo: string;
|
||||||
|
packageJSON: PackageJSON;
|
||||||
|
repoUrl: string;
|
||||||
|
}> => {
|
||||||
|
const [owner, repo] = repository.split('/');
|
||||||
|
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
path: 'package.json',
|
||||||
|
ref: commitHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!packageJSONData) {
|
||||||
|
throw new Error('Package.json file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
||||||
|
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
|
||||||
|
|
||||||
|
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||||
|
|
||||||
|
const repoUrl = (
|
||||||
|
await octokit.rest.repos.get({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
})
|
||||||
|
).data.html_url;
|
||||||
|
|
||||||
|
return {
|
||||||
|
repo,
|
||||||
|
packageJSON,
|
||||||
|
repoUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper method for registry txs to retry once if 'account sequence mismatch' occurs
|
||||||
|
export const registryTransactionWithRetry = async (
|
||||||
|
txMethod: () => Promise<any>
|
||||||
|
): Promise<any> => {
|
||||||
|
try {
|
||||||
|
return await txMethod();
|
||||||
|
} catch (error: any) {
|
||||||
|
if (!error.message.includes('account sequence mismatch')) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`Transaction failed due to account sequence mismatch. Retrying...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await txMethod();
|
||||||
|
} catch (retryError: any) {
|
||||||
|
throw new Error(`Transaction failed again after retry: ${retryError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,8 +2,6 @@ import * as fs from 'fs/promises';
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
|
||||||
import { getConfig } from '../src/utils';
|
import { getConfig } from '../src/utils';
|
||||||
import { Config } from '../src/config';
|
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
|
||||||
|
|
||||||
const log = debug('snowball:delete-database');
|
const log = debug('snowball:delete-database');
|
||||||
|
|
||||||
@ -13,7 +11,7 @@ const deleteFile = async (filePath: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const config = await getConfig();
|
||||||
|
|
||||||
deleteFile(config.database.dbPath);
|
deleteFile(config.database.dbPath);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
||||||
"name": "Snowball Tools",
|
"name": "Deploy Tools",
|
||||||
"slug": "snowball-tools-1"
|
"slug": "deploy-tools"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
|
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
|
||||||
|
@ -16,8 +16,6 @@ import {
|
|||||||
getEntities,
|
getEntities,
|
||||||
loadAndSaveData
|
loadAndSaveData
|
||||||
} from '../src/utils';
|
} from '../src/utils';
|
||||||
import { Config } from '../src/config';
|
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
|
||||||
|
|
||||||
const log = debug('snowball:initialize-database');
|
const log = debug('snowball:initialize-database');
|
||||||
|
|
||||||
@ -156,7 +154,7 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const config = await getConfig();
|
||||||
const isDbPresent = await checkFileExists(config.database.dbPath);
|
const isDbPresent = await checkFileExists(config.database.dbPath);
|
||||||
|
|
||||||
if (!isDbPresent) {
|
if (!isDbPresent) {
|
||||||
|
@ -1,39 +1,40 @@
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
|
||||||
import { Registry } from '@snowballtools/laconic-sdk';
|
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||||
|
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
|
||||||
import { Config } from '../src/config';
|
|
||||||
import { getConfig } from '../src/utils';
|
import { getConfig } from '../src/utils';
|
||||||
|
|
||||||
const log = debug('snowball:initialize-registry');
|
const log = debug('snowball:initialize-registry');
|
||||||
|
|
||||||
const DENOM = 'aphoton';
|
const DENOM = 'alnt';
|
||||||
const BOND_AMOUNT = '1000000000';
|
const BOND_AMOUNT = '1000000000';
|
||||||
|
|
||||||
async function main () {
|
async function main () {
|
||||||
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { registryConfig } = await getConfig();
|
||||||
|
|
||||||
// TODO: Get authority names from args
|
// TODO: Get authority names from args
|
||||||
const authorityNames = ['snowballtools', registryConfig.authority];
|
const authorityNames = ['snowballtools', registryConfig.authority];
|
||||||
|
|
||||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, {chainId: registryConfig.chainId});
|
||||||
|
|
||||||
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
||||||
log('bondId:', bondId);
|
log('bondId:', bondId);
|
||||||
|
|
||||||
|
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||||
|
|
||||||
await registry.createBond(
|
await registry.createBond(
|
||||||
{ denom: DENOM, amount: BOND_AMOUNT },
|
{ denom: DENOM, amount: BOND_AMOUNT },
|
||||||
registryConfig.privateKey,
|
registryConfig.privateKey,
|
||||||
registryConfig.fee
|
fee
|
||||||
);
|
);
|
||||||
|
|
||||||
for await (const name of authorityNames) {
|
for await (const name of authorityNames) {
|
||||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
|
await registry.reserveAuthority({ name }, registryConfig.privateKey, fee);
|
||||||
log('Reserved authority name:', name);
|
log('Reserved authority name:', name);
|
||||||
await registry.setAuthorityBond(
|
await registry.setAuthorityBond(
|
||||||
{ name, bondId },
|
{ name, bondId },
|
||||||
registryConfig.privateKey,
|
registryConfig.privateKey,
|
||||||
registryConfig.fee
|
fee
|
||||||
);
|
);
|
||||||
log(`Bond ${bondId} set for authority ${name}`);
|
log(`Bond ${bondId} set for authority ${name}`);
|
||||||
}
|
}
|
||||||
|
@ -2,22 +2,20 @@ import debug from 'debug';
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { Registry } from '@snowballtools/laconic-sdk';
|
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||||
|
|
||||||
import { Config } from '../src/config';
|
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
|
||||||
import { getConfig } from '../src/utils';
|
import { getConfig } from '../src/utils';
|
||||||
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
|
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
|
||||||
|
|
||||||
const log = debug('snowball:publish-deploy-records');
|
const log = debug('snowball:publish-deploy-records');
|
||||||
|
|
||||||
async function main () {
|
async function main() {
|
||||||
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { registryConfig, database, misc } = await getConfig();
|
||||||
|
|
||||||
const registry = new Registry(
|
const registry = new Registry(
|
||||||
registryConfig.gqlEndpoint,
|
registryConfig.gqlEndpoint,
|
||||||
registryConfig.restEndpoint,
|
registryConfig.restEndpoint,
|
||||||
registryConfig.chainId
|
{ chainId: registryConfig.chainId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const dataSource = new DataSource({
|
const dataSource = new DataSource({
|
||||||
@ -40,7 +38,7 @@ async function main () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for await (const deployment of deployments) {
|
for await (const deployment of deployments) {
|
||||||
const url = `https://${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
|
const url = `https://${(deployment.project.name).toLowerCase()}-${deployment.id}.${deployment.deployer.baseDomain}`;
|
||||||
|
|
||||||
const applicationDeploymentRecord = {
|
const applicationDeploymentRecord = {
|
||||||
type: 'ApplicationDeploymentRecord',
|
type: 'ApplicationDeploymentRecord',
|
||||||
@ -61,6 +59,8 @@ async function main () {
|
|||||||
url
|
url
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||||
|
|
||||||
const result = await registry.setRecord(
|
const result = await registry.setRecord(
|
||||||
{
|
{
|
||||||
privateKey: registryConfig.privateKey,
|
privateKey: registryConfig.privateKey,
|
||||||
@ -68,12 +68,12 @@ async function main () {
|
|||||||
bondId: registryConfig.bondId
|
bondId: registryConfig.bondId
|
||||||
},
|
},
|
||||||
'',
|
'',
|
||||||
registryConfig.fee
|
fee
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove deployment for project subdomain if deployment is for production environment
|
// Remove deployment for project subdomain if deployment is for production environment
|
||||||
if (deployment.environment === Environment.Production) {
|
if (deployment.environment === Environment.Production) {
|
||||||
applicationDeploymentRecord.url = `https://${deployment.project.subDomain}`
|
applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.deployer.baseDomain}`;
|
||||||
|
|
||||||
await registry.setRecord(
|
await registry.setRecord(
|
||||||
{
|
{
|
||||||
@ -82,12 +82,12 @@ async function main () {
|
|||||||
bondId: registryConfig.bondId
|
bondId: registryConfig.bondId
|
||||||
},
|
},
|
||||||
'',
|
'',
|
||||||
registryConfig.fee
|
fee
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
log('Application deployment record data:', applicationDeploymentRecord);
|
log('Application deployment record data:', applicationDeploymentRecord);
|
||||||
log(`Application deployment record published: ${result.data.id}`);
|
log(`Application deployment record published: ${result.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,22 +2,20 @@ import debug from 'debug';
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { Registry } from '@cerc-io/laconic-sdk';
|
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||||
|
|
||||||
import { Config } from '../src/config';
|
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
|
||||||
import { getConfig } from '../src/utils';
|
import { getConfig } from '../src/utils';
|
||||||
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
||||||
|
|
||||||
const log = debug('snowball:publish-deployment-removal-records');
|
const log = debug('snowball:publish-deployment-removal-records');
|
||||||
|
|
||||||
async function main () {
|
async function main () {
|
||||||
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { registryConfig, database } = await getConfig();
|
||||||
|
|
||||||
const registry = new Registry(
|
const registry = new Registry(
|
||||||
registryConfig.gqlEndpoint,
|
registryConfig.gqlEndpoint,
|
||||||
registryConfig.restEndpoint,
|
registryConfig.restEndpoint,
|
||||||
registryConfig.chainId
|
{ chainId: registryConfig.chainId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const dataSource = new DataSource({
|
const dataSource = new DataSource({
|
||||||
@ -47,6 +45,8 @@ async function main () {
|
|||||||
request: deployment.applicationDeploymentRemovalRequestId,
|
request: deployment.applicationDeploymentRemovalRequestId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||||
|
|
||||||
const result = await registry.setRecord(
|
const result = await registry.setRecord(
|
||||||
{
|
{
|
||||||
privateKey: registryConfig.privateKey,
|
privateKey: registryConfig.privateKey,
|
||||||
@ -54,11 +54,11 @@ async function main () {
|
|||||||
bondId: registryConfig.bondId
|
bondId: registryConfig.bondId
|
||||||
},
|
},
|
||||||
'',
|
'',
|
||||||
registryConfig.fee
|
fee
|
||||||
);
|
);
|
||||||
|
|
||||||
log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
|
log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
|
||||||
log(`Application deployment removal record published: ${result.data.id}`);
|
log(`Application deployment removal record published: ${result.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
packages/deployer/.env.example
Normal file
3
packages/deployer/.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
REGISTRY_BOND_ID=
|
||||||
|
DEPLOYER_LRN=
|
||||||
|
AUTHORITY=
|
@ -1,9 +1,11 @@
|
|||||||
# deployer
|
# deployer
|
||||||
|
|
||||||
- Install dependencies
|
- Install dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install jq # if you do not have jq installed already
|
brew install jq # if you do not have jq installed already
|
||||||
```
|
```
|
||||||
@ -12,13 +14,13 @@
|
|||||||
|
|
||||||
- To deploy frontend app to `dashboard.staging.apps.snowballtools.com`
|
- To deploy frontend app to `dashboard.staging.apps.snowballtools.com`
|
||||||
|
|
||||||
```
|
```bash
|
||||||
./deploy-frontend.staging.sh
|
./deploy-frontend.staging.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
- To deploy frontend app to `dashboard.apps.snowballtools.com`
|
- To deploy frontend app to `dashboard.apps.snowballtools.com`
|
||||||
|
|
||||||
```
|
```bash
|
||||||
./deploy-frontend.sh
|
./deploy-frontend.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -27,6 +29,7 @@
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
record:
|
record:
|
||||||
...
|
...
|
||||||
@ -35,6 +38,7 @@
|
|||||||
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
- On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too
|
- On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too
|
||||||
|
|
||||||
## Troubleshoot
|
## Troubleshoot
|
||||||
@ -44,14 +48,17 @@
|
|||||||
|
|
||||||
- If deployment fails due to low bond balance
|
- If deployment fails due to low bond balance
|
||||||
- Check balances
|
- Check balances
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Account balance
|
# Account balance
|
||||||
yarn laconic cns account get
|
yarn laconic registry account get
|
||||||
|
|
||||||
# Bond balance
|
# Bond balance
|
||||||
yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
yarn laconic registry bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||||
```
|
```
|
||||||
|
|
||||||
- Command to refill bond
|
- Command to refill bond
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
|
yarn laconic registry bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type alnt --quantity 10000000
|
||||||
```
|
```
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
services:
|
services:
|
||||||
cns:
|
registry:
|
||||||
restEndpoint: 'http://console.laconic.com:1317'
|
restEndpoint: 'http://console.laconic.com:1317'
|
||||||
gqlEndpoint: 'http://console.laconic.com:9473/api'
|
gqlEndpoint: 'http://console.laconic.com:9473/api'
|
||||||
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
|
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
|
||||||
bondId: 098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661
|
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
|
||||||
chainId: laconic_9000-1
|
chainId: laconic_9000-1
|
||||||
gas: 550000
|
gas:
|
||||||
fees: 200000aphoton
|
fees:
|
||||||
|
gasPrice: 1
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
cns:
|
registry:
|
||||||
restEndpoint: http://console.laconic.com:1317
|
rpcEndpoint: https://laconicd-sapo.laconic.com
|
||||||
gqlEndpoint: http://console.laconic.com:9473/api
|
gqlEndpoint: https://laconicd-sapo.laconic.com/api
|
||||||
userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
|
userKey:
|
||||||
bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
bondId:
|
||||||
chainId: laconic_9000-1
|
chainId: laconic_9000-2
|
||||||
gas: 1200000
|
gasPrice: 1alnt
|
||||||
fees: 200000aphoton
|
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
source .env
|
||||||
|
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
||||||
|
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||||
|
echo "Using AUTHORITY: $AUTHORITY"
|
||||||
|
|
||||||
# Repository URL
|
# Repository URL
|
||||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||||
|
|
||||||
@ -13,83 +18,131 @@ PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
|||||||
CURRENT_DATE_TIME=$(date -u)
|
CURRENT_DATE_TIME=$(date -u)
|
||||||
|
|
||||||
CONFIG_FILE=config.yml
|
CONFIG_FILE=config.yml
|
||||||
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
|
|
||||||
|
|
||||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||||
|
|
||||||
# Get latest version from registry and increment application-record version
|
# Get latest version from registry and increment application-record version
|
||||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "deploy-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||||
|
|
||||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||||
# Set application-record version if no previous records were found
|
# Set application-record version if no previous records were found
|
||||||
NEW_APPLICATION_VERSION=0.0.1
|
NEW_APPLICATION_VERSION=0.0.1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate application-deployment-request.yml
|
|
||||||
cat > ./records/application-deployment-request.yml <<EOF
|
|
||||||
record:
|
|
||||||
type: ApplicationDeploymentRequest
|
|
||||||
version: '1.0.0'
|
|
||||||
name: snowballtools-base-frontend@$PACKAGE_VERSION
|
|
||||||
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
|
|
||||||
dns: dashboard
|
|
||||||
config:
|
|
||||||
env:
|
|
||||||
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
|
||||||
LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab
|
|
||||||
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
|
||||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
|
|
||||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
|
||||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
|
||||||
meta:
|
|
||||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
|
||||||
repository: "$REPO_URL"
|
|
||||||
repository_ref: $LATEST_HASH
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Generate application-record.yml with incremented version
|
# Generate application-record.yml with incremented version
|
||||||
cat > ./records/application-record.yml <<EOF
|
cat >./records/application-record.yml <<EOF
|
||||||
record:
|
record:
|
||||||
type: ApplicationRecord
|
type: ApplicationRecord
|
||||||
version: $NEW_APPLICATION_VERSION
|
version: $NEW_APPLICATION_VERSION
|
||||||
repository_ref: $LATEST_HASH
|
repository_ref: $LATEST_HASH
|
||||||
repository: ["$REPO_URL"]
|
repository: ["$REPO_URL"]
|
||||||
app_type: webapp
|
app_type: webapp
|
||||||
name: snowballtools-base-frontend
|
name: deploy-frontend
|
||||||
app_version: $PACKAGE_VERSION
|
app_version: $PACKAGE_VERSION
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Files generated successfully."
|
echo "Files generated successfully"
|
||||||
|
|
||||||
RECORD_FILE=records/application-record.yml
|
RECORD_FILE=records/application-record.yml
|
||||||
|
|
||||||
# Publish ApplicationRecord
|
# Publish ApplicationRecord
|
||||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to publish record"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
RECORD_ID=$(echo $publish_response | jq -r '.id')
|
||||||
echo "ApplicationRecord published"
|
echo "ApplicationRecord published"
|
||||||
echo $RECORD_ID
|
echo $RECORD_ID
|
||||||
|
|
||||||
# Set name to record
|
# Set name to record
|
||||||
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
|
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
sleep 2
|
sleep 2
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set hash"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
sleep 2
|
sleep 2
|
||||||
# Set name if latest release
|
# Set name if latest release
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set release"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||||
|
|
||||||
# Check if record found for REGISTRY_APP_CRN
|
# Check if record found for REGISTRY_APP_LRN
|
||||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to query name"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||||
echo "No record found for $REGISTRY_APP_CRN."
|
echo "No record found for $REGISTRY_APP_LRN."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Get payment address for deployer
|
||||||
|
paymentAddress=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.paymentAddress')
|
||||||
|
paymentAmount=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.minimumPayment' | sed 's/alnt//g')
|
||||||
|
# Pay deployer if paymentAmount is not null
|
||||||
|
if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then
|
||||||
|
payment=$(yarn --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount")
|
||||||
|
|
||||||
|
# Extract the transaction hash
|
||||||
|
txHash=$(echo "$payment" | jq -r '.tx.hash')
|
||||||
|
echo "Paid deployer with txHash as $txHash"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "Payment amount is null; skipping payment."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate application-deployment-request.yml
|
||||||
|
cat >./records/application-deployment-request.yml <<EOF
|
||||||
|
record:
|
||||||
|
type: ApplicationDeploymentRequest
|
||||||
|
version: '1.0.0'
|
||||||
|
name: deploy-frontend@$PACKAGE_VERSION
|
||||||
|
application: lrn://$AUTHORITY/applications/deploy-frontend@$PACKAGE_VERSION
|
||||||
|
deployer: $DEPLOYER_LRN
|
||||||
|
dns: deploy
|
||||||
|
config:
|
||||||
|
env:
|
||||||
|
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.apps.vaasl.io
|
||||||
|
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liaet4yc0KX0iM1c
|
||||||
|
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||||
|
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||||
|
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
|
||||||
|
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||||
|
meta:
|
||||||
|
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||||
|
repository: "$REPO_URL"
|
||||||
|
repository_ref: $LATEST_HASH
|
||||||
|
payment: $txHash
|
||||||
|
EOF
|
||||||
|
|
||||||
RECORD_FILE=records/application-deployment-request.yml
|
RECORD_FILE=records/application-deployment-request.yml
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to query deployment request"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
|
||||||
echo "ApplicationDeploymentRequest published"
|
echo "ApplicationDeploymentRequest published"
|
||||||
echo $DEPLOYMENT_REQUEST_ID
|
echo $DEPLOYMENT_REQUEST_ID
|
||||||
|
@ -18,7 +18,7 @@ REGISTRY_BOND_ID="098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb466
|
|||||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||||
|
|
||||||
# Get latest version from registry and increment application-record version
|
# Get latest version from registry and increment application-record version
|
||||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "staging-snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "staging-snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||||
|
|
||||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||||
# Set application-record version if no previous records were found
|
# Set application-record version if no previous records were found
|
||||||
@ -26,26 +26,27 @@ if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; t
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate application-deployment-request.yml
|
# Generate application-deployment-request.yml
|
||||||
cat > ./staging-records/application-deployment-request.yml <<EOF
|
cat >./staging-records/application-deployment-request.yml <<EOF
|
||||||
record:
|
record:
|
||||||
type: ApplicationDeploymentRequest
|
type: ApplicationDeploymentRequest
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
name: staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
name: staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
||||||
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
application: lrn://staging-snowballtools/applications/staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
||||||
dns: dashboard.staging.apps.snowballtools.com
|
dns: dashboard.staging.apps.snowballtools.com
|
||||||
config:
|
config:
|
||||||
env:
|
env:
|
||||||
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
|
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
|
||||||
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
|
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV
|
||||||
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
|
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
|
||||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example
|
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
|
||||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
|
||||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||||
|
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
||||||
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
|
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
|
||||||
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
||||||
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
||||||
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
|
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.staging.apps.snowballtools.com
|
||||||
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
|
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
|
||||||
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
|
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
|
||||||
meta:
|
meta:
|
||||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||||
@ -54,7 +55,7 @@ record:
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Generate application-record.yml with incremented version
|
# Generate application-record.yml with incremented version
|
||||||
cat > ./staging-records/application-record.yml <<EOF
|
cat >./staging-records/application-record.yml <<EOF
|
||||||
record:
|
record:
|
||||||
type: ApplicationRecord
|
type: ApplicationRecord
|
||||||
version: $NEW_APPLICATION_VERSION
|
version: $NEW_APPLICATION_VERSION
|
||||||
@ -70,32 +71,64 @@ echo "Files generated successfully."
|
|||||||
RECORD_FILE=staging-records/application-record.yml
|
RECORD_FILE=staging-records/application-record.yml
|
||||||
|
|
||||||
# Publish ApplicationRecord
|
# Publish ApplicationRecord
|
||||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to publish record"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
RECORD_ID=$(echo $publish_response | jq -r '.id')
|
||||||
echo "ApplicationRecord published"
|
echo "ApplicationRecord published"
|
||||||
echo $RECORD_ID
|
echo $RECORD_ID
|
||||||
|
|
||||||
# Set name to record
|
# Set name to record
|
||||||
REGISTRY_APP_CRN="crn://staging-snowballtools/applications/staging-snowballtools-base-frontend"
|
REGISTRY_APP_LRN="lrn://staging-snowballtools/applications/staging-snowballtools-base-frontend"
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
sleep 2
|
sleep 2
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set hash"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
sleep 2
|
sleep 2
|
||||||
# Set name if latest release
|
# Set name if latest release
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to set release"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||||
|
|
||||||
# Check if record found for REGISTRY_APP_CRN
|
# Check if record found for REGISTRY_APP_LRN
|
||||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
||||||
|
rc=$?
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to query name"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||||
echo "No record found for $REGISTRY_APP_CRN."
|
echo "No record found for $REGISTRY_APP_LRN."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
RECORD_FILE=staging-records/application-deployment-request.yml
|
RECORD_FILE=staging-records/application-deployment-request.yml
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||||
|
if [ $rc -ne 0 ]; then
|
||||||
|
echo "FATAL: Failed to query deployment request"
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
|
||||||
echo "ApplicationDeploymentRequest published"
|
echo "ApplicationDeploymentRequest published"
|
||||||
echo $DEPLOYMENT_REQUEST_ID
|
echo $DEPLOYMENT_REQUEST_ID
|
||||||
|
@ -4,6 +4,6 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@snowballtools/laconic-registry-cli": "^0.1.13"
|
"@cerc-io/laconic-registry-cli": "^0.2.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
record:
|
record:
|
||||||
type: ApplicationDeploymentRequest
|
type: ApplicationDeploymentRequest
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
name: snowballtools-base-frontend@0.1.8
|
name: deploy-frontend@1.0.0
|
||||||
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
|
application: lrn://vaasl/applications/deploy-frontend@1.0.0
|
||||||
dns: dashboard
|
dns: deploy
|
||||||
config:
|
config:
|
||||||
env:
|
env:
|
||||||
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.apps.vaasl.io
|
||||||
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liaet4yc0KX0iM1c
|
||||||
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
||||||
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
|
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
||||||
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
|
||||||
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
|
||||||
meta:
|
meta:
|
||||||
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
|
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
|
||||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||||
|
@ -4,5 +4,5 @@ record:
|
|||||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
||||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||||
app_type: webapp
|
app_type: webapp
|
||||||
name: snowballtools-base-frontend
|
name: deploy-frontend
|
||||||
app_version: 0.1.8
|
app_version: 1.0.0
|
||||||
|
@ -7,18 +7,18 @@ record:
|
|||||||
config:
|
config:
|
||||||
env:
|
env:
|
||||||
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
|
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
|
||||||
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
|
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV
|
||||||
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
|
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
|
||||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example
|
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
|
||||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
|
||||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||||
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
|
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
|
||||||
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
||||||
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
||||||
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
|
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.staging.apps.snowballtools.com
|
||||||
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
|
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
|
||||||
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
|
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
|
||||||
meta:
|
meta:
|
||||||
note: Added by Snowball @ Tuesday 21 May 2024 06:17:23 AM UTC
|
note: Added by Snowball @ Mon Jun 24 23:51:48 UTC 2024
|
||||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||||
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
|
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
record:
|
record:
|
||||||
type: ApplicationRecord
|
type: ApplicationRecord
|
||||||
version: 0.0.2
|
version: 0.0.1
|
||||||
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
|
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
|
||||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||||
app_type: webapp
|
app_type: webapp
|
||||||
name: staging-snowballtools-base-frontend
|
name: staging-snowballtools-base-frontend
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Check if the live web app deployer is in a working state
|
Check if the live web app deployer is in a working state
|
||||||
|
|
||||||
- Web app repo used: https://github.com/snowball-tools-platform/test-progressive-web-app (main branch)
|
- Web app repo used: <https://github.com/snowball-tools/test-progressive-web-app> (main branch)
|
||||||
- Config used: [../config.yml](../config.yml)
|
- Config used: [../config.yml](../config.yml)
|
||||||
- The script [test-webapp-deployment-undeployment.sh](./test-webapp-deployment-undeployment.sh) performs the following:
|
- The script [test-webapp-deployment-undeployment.sh](./test-webapp-deployment-undeployment.sh) performs the following:
|
||||||
- Create / update [`ApplicationRecord`](./records/application-record.yml) and [`ApplicationDeploymentRequest`](./records/application-deployment-request.yml) records with latest meta data from the repo
|
- Create / update [`ApplicationRecord`](./records/application-record.yml) and [`ApplicationDeploymentRequest`](./records/application-deployment-request.yml) records with latest meta data from the repo
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
record:
|
record:
|
||||||
type: ApplicationDeploymentRequest
|
type: ApplicationDeploymentRequest
|
||||||
version: '1.0.0'
|
version: "1.0.0"
|
||||||
name: deployment-test-app@0.1.24
|
name: deployment-test-app@0.1.24
|
||||||
application: crn://snowballtools/applications/deployment-test-app@0.1.24
|
application: crn://snowballtools/applications/deployment-test-app@0.1.24
|
||||||
dns: deployment-ci-test
|
dns: deployment-ci-test
|
||||||
@ -11,5 +11,5 @@ record:
|
|||||||
CERC_WEBAPP_DEBUG: 0
|
CERC_WEBAPP_DEBUG: 0
|
||||||
meta:
|
meta:
|
||||||
note: Deployment test @ Thu 11 Apr 2024 07:29:19 AM UTC
|
note: Deployment test @ Thu 11 Apr 2024 07:29:19 AM UTC
|
||||||
repository: "https://github.com/snowball-tools-platform/test-progressive-web-app"
|
repository: "https://github.com/snowball-tools/test-progressive-web-app"
|
||||||
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
||||||
|
@ -2,7 +2,7 @@ record:
|
|||||||
type: ApplicationRecord
|
type: ApplicationRecord
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
||||||
repository: ["https://github.com/snowball-tools-platform/test-progressive-web-app"]
|
repository: ["https://github.com/snowball-tools/test-progressive-web-app"]
|
||||||
app_type: webapp
|
app_type: webapp
|
||||||
name: deployment-test-app
|
name: deployment-test-app
|
||||||
app_version: 0.1.24
|
app_version: 0.1.24
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Repository URL
|
# Repository URL
|
||||||
REPO_URL="https://github.com/snowball-tools-platform/test-progressive-web-app"
|
REPO_URL="https://github.com/snowball-tools/test-progressive-web-app"
|
||||||
|
|
||||||
# Get the latest commit hash from the repository
|
# Get the latest commit hash from the repository
|
||||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
||||||
@ -22,7 +22,7 @@ REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be
|
|||||||
APP_NAME=deployment-test-app
|
APP_NAME=deployment-test-app
|
||||||
|
|
||||||
# Get latest version from registry and increment application-record version
|
# Get latest version from registry and increment application-record version
|
||||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||||
|
|
||||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||||
# Set application-record version if no previous records were found
|
# Set application-record version if no previous records were found
|
||||||
@ -32,7 +32,7 @@ fi
|
|||||||
# Generate application-record.yml with incremented version
|
# Generate application-record.yml with incremented version
|
||||||
RECORD_FILE=packages/deployer/test/records/application-record.yml
|
RECORD_FILE=packages/deployer/test/records/application-record.yml
|
||||||
|
|
||||||
cat > $RECORD_FILE <<EOF
|
cat >$RECORD_FILE <<EOF
|
||||||
record:
|
record:
|
||||||
type: ApplicationRecord
|
type: ApplicationRecord
|
||||||
version: $NEW_APPLICATION_VERSION
|
version: $NEW_APPLICATION_VERSION
|
||||||
@ -46,12 +46,12 @@ EOF
|
|||||||
# Generate application-deployment-request.yml
|
# Generate application-deployment-request.yml
|
||||||
REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-request.yml
|
REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-request.yml
|
||||||
|
|
||||||
cat > $REQUEST_RECORD_FILE <<EOF
|
cat >$REQUEST_RECORD_FILE <<EOF
|
||||||
record:
|
record:
|
||||||
type: ApplicationDeploymentRequest
|
type: ApplicationDeploymentRequest
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
name: $APP_NAME@$PACKAGE_VERSION
|
name: $APP_NAME@$PACKAGE_VERSION
|
||||||
application: crn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
|
application: lrn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
|
||||||
dns: deployment-ci-test
|
dns: deployment-ci-test
|
||||||
config:
|
config:
|
||||||
env:
|
env:
|
||||||
@ -67,31 +67,31 @@ EOF
|
|||||||
echo "Record files generated successfully."
|
echo "Record files generated successfully."
|
||||||
|
|
||||||
# Publish ApplicationRecord
|
# Publish ApplicationRecord
|
||||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE | jq -r '.id')
|
||||||
echo "ApplicationRecord published"
|
echo "ApplicationRecord published"
|
||||||
echo $RECORD_ID
|
echo $RECORD_ID
|
||||||
|
|
||||||
# Set name to record
|
# Set name to record
|
||||||
REGISTRY_APP_CRN="crn://snowballtools/applications/$APP_NAME"
|
REGISTRY_APP_LRN="lrn://snowballtools/applications/$APP_NAME"
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||||
sleep 2
|
sleep 2
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||||
sleep 2
|
sleep 2
|
||||||
# Set name if latest release
|
# Set name if latest release
|
||||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||||
|
|
||||||
# Check if record exists for REGISTRY_APP_CRN
|
# Check if record exists for REGISTRY_APP_LRN
|
||||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN" | jq '.[0]')
|
||||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||||
echo "No record found for $REGISTRY_APP_CRN."
|
echo "No record found for $REGISTRY_APP_LRN."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
|
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
|
||||||
echo "ApplicationDeploymentRequest published"
|
echo "ApplicationDeploymentRequest published"
|
||||||
echo $DEPLOYMENT_REQUEST_ID
|
echo $DEPLOYMENT_REQUEST_ID
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ MAX_RETRIES=20
|
|||||||
# Check that a ApplicationDeploymentRecord is published
|
# Check that a ApplicationDeploymentRecord is published
|
||||||
retry_count=0
|
retry_count=0
|
||||||
while true; do
|
while true; do
|
||||||
deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
|
deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
|
||||||
len_deployment_records=$(echo $deployment_records_response | jq 'length')
|
len_deployment_records=$(echo $deployment_records_response | jq 'length')
|
||||||
|
|
||||||
# Check if number of records returned is 0
|
# Check if number of records returned is 0
|
||||||
@ -115,7 +115,7 @@ while true; do
|
|||||||
else
|
else
|
||||||
echo "ApplicationDeploymentRecord not found, retrying in $RETRY_INTERVAL sec..."
|
echo "ApplicationDeploymentRecord not found, retrying in $RETRY_INTERVAL sec..."
|
||||||
sleep $RETRY_INTERVAL
|
sleep $RETRY_INTERVAL
|
||||||
retry_count=$((retry_count+1))
|
retry_count=$((retry_count + 1))
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "ApplicationDeploymentRecord found"
|
echo "ApplicationDeploymentRecord found"
|
||||||
@ -140,7 +140,7 @@ fetched_url=$(echo $deployment_records_response | jq -r '.[0].attributes.url')
|
|||||||
|
|
||||||
retry_count=0
|
retry_count=0
|
||||||
max_retries=10
|
max_retries=10
|
||||||
retry_interval=5
|
retry_interval=10
|
||||||
while true; do
|
while true; do
|
||||||
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
||||||
if [ "$url_response" = "200" ]; then
|
if [ "$url_response" = "200" ]; then
|
||||||
@ -154,7 +154,7 @@ while true; do
|
|||||||
else
|
else
|
||||||
echo "Deployment URL $fetched_url is not active, received code $url_response, retrying in $retry_interval sec..."
|
echo "Deployment URL $fetched_url is not active, received code $url_response, retrying in $retry_interval sec..."
|
||||||
sleep $retry_interval
|
sleep $retry_interval
|
||||||
retry_count=$((retry_count+1))
|
retry_count=$((retry_count + 1))
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@ -162,7 +162,7 @@ done
|
|||||||
# Generate application-deployment-removal-request.yml
|
# Generate application-deployment-removal-request.yml
|
||||||
REMOVAL_REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-removal-request.yml
|
REMOVAL_REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-removal-request.yml
|
||||||
|
|
||||||
cat > $REMOVAL_REQUEST_RECORD_FILE <<EOF
|
cat >$REMOVAL_REQUEST_RECORD_FILE <<EOF
|
||||||
record:
|
record:
|
||||||
deployment: $DEPLOYMENT_RECORD_ID
|
deployment: $DEPLOYMENT_RECORD_ID
|
||||||
type: ApplicationDeploymentRemovalRequest
|
type: ApplicationDeploymentRemovalRequest
|
||||||
@ -170,14 +170,14 @@ record:
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
||||||
echo "ApplicationDeploymentRemovalRequest published"
|
echo "ApplicationDeploymentRemovalRequest published"
|
||||||
echo $REMOVAL_REQUEST_ID
|
echo $REMOVAL_REQUEST_ID
|
||||||
|
|
||||||
# Check that an ApplicationDeploymentRemovalRecord is published
|
# Check that an ApplicationDeploymentRemovalRecord is published
|
||||||
retry_count=0
|
retry_count=0
|
||||||
while true; do
|
while true; do
|
||||||
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
||||||
len_removal_records=$(echo $removal_records_response | jq 'length')
|
len_removal_records=$(echo $removal_records_response | jq 'length')
|
||||||
|
|
||||||
# Check if number of records returned is 0
|
# Check if number of records returned is 0
|
||||||
@ -190,7 +190,7 @@ while true; do
|
|||||||
else
|
else
|
||||||
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
|
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
|
||||||
sleep $RETRY_INTERVAL
|
sleep $RETRY_INTERVAL
|
||||||
retry_count=$((retry_count+1))
|
retry_count=$((retry_count + 1))
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "ApplicationDeploymentRemovalRecord found"
|
echo "ApplicationDeploymentRemovalRecord found"
|
||||||
@ -217,7 +217,7 @@ while true; do
|
|||||||
else
|
else
|
||||||
echo "Deployment URL $fetched_url is still active, received code $url_response, retrying in $retry_interval sec..."
|
echo "Deployment URL $fetched_url is still active, received code $url_response, retrying in $retry_interval sec..."
|
||||||
sleep $retry_interval
|
sleep $retry_interval
|
||||||
retry_count=$((retry_count+1))
|
retry_count=$((retry_count + 1))
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
@ -14,4 +14,6 @@ VITE_BUGSNAG_API_KEY=
|
|||||||
|
|
||||||
VITE_PASSKEY_WALLET_RPID=
|
VITE_PASSKEY_WALLET_RPID=
|
||||||
VITE_TURNKEY_API_BASE_URL=
|
VITE_TURNKEY_API_BASE_URL=
|
||||||
VITE_TURNKEY_ORGANIZATION_ID=
|
VITE_TURNKEY_ORGANIZATION_ID=
|
||||||
|
|
||||||
|
VITE_LACONICD_CHAIN_ID=
|
||||||
|
1
packages/frontend/.prettierignore
Normal file
1
packages/frontend/.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist/
|
@ -9,6 +9,7 @@ import { join, dirname } from 'path';
|
|||||||
function getAbsolutePath(value: string): any {
|
function getAbsolutePath(value: string): any {
|
||||||
return dirname(require.resolve(join(value, 'package.json')));
|
return dirname(require.resolve(join(value, 'package.json')));
|
||||||
}
|
}
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||||
addons: [
|
addons: [
|
||||||
@ -26,6 +27,7 @@ const config: StorybookConfig = {
|
|||||||
docs: {
|
docs: {
|
||||||
autodocs: 'tag',
|
autodocs: 'tag',
|
||||||
},
|
},
|
||||||
|
staticDirs: ['../public'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
@ -1,30 +1,63 @@
|
|||||||
# React + TypeScript + Vite
|
# frontend
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
This is a [vite](https://vitejs.dev/) [react](https://reactjs.org/) [nextjs](https://nextjs.org/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
## Getting Started
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
### Install dependencies
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
In the root of the project, run:
|
||||||
|
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
```zsh
|
||||||
|
yarn
|
||||||
- Configure the top-level `parserOptions` property like this:
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default {
|
|
||||||
// other rules...
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module',
|
|
||||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
### Build backend
|
||||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
|
||||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
```zsh
|
||||||
|
yarn build --ignore frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
#### Local
|
||||||
|
|
||||||
|
Copy the `.env.example` file to `.env`:
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Staging environment variables
|
||||||
|
|
||||||
|
Change in [deployer/deploy-frontend.staging.sh](/packages/deployer/deploy-frontend.staging.sh)
|
||||||
|
|
||||||
|
#### Production environment variables
|
||||||
|
|
||||||
|
Change in [deployer/deploy-frontend.sh](/packages/deployer/deploy-frontend.sh)
|
||||||
|
|
||||||
|
### Run development server
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
From the root of the project,
|
||||||
|
|
||||||
|
### Staging
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
cd packages/deployer && ./deploy-frontend.staging.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
cd packages/deployer && ./deploy-frontend.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment status
|
||||||
|
|
||||||
|
Check the status of the deployment [here](https://webapp-deployer.apps.snowballtools.com)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 3000",
|
"dev": "vite --port 3000",
|
||||||
@ -16,8 +16,11 @@
|
|||||||
"@bugsnag/browser-performance": "^2.4.1",
|
"@bugsnag/browser-performance": "^2.4.1",
|
||||||
"@bugsnag/js": "^7.22.7",
|
"@bugsnag/js": "^7.22.7",
|
||||||
"@bugsnag/plugin-react": "^7.22.7",
|
"@bugsnag/plugin-react": "^7.22.7",
|
||||||
|
"@emotion/react": "^11.13.3",
|
||||||
|
"@emotion/styled": "^11.13.0",
|
||||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||||
"@fontsource/inter": "^5.0.16",
|
"@fontsource/inter": "^5.0.16",
|
||||||
|
"@mui/material": "^6.1.3",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
@ -27,10 +30,6 @@
|
|||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@snowballtools/auth": "^0.2.0",
|
|
||||||
"@snowballtools/auth-lit": "^0.2.0",
|
|
||||||
"@snowballtools/js-sdk": "^0.1.1",
|
|
||||||
"@snowballtools/link-lit-alchemy-light": "^0.2.0",
|
|
||||||
"@snowballtools/material-tailwind-react-fork": "^2.1.10",
|
"@snowballtools/material-tailwind-react-fork": "^2.1.10",
|
||||||
"@snowballtools/smartwallet-alchemy-light": "^0.2.0",
|
"@snowballtools/smartwallet-alchemy-light": "^0.2.0",
|
||||||
"@snowballtools/types": "^0.2.0",
|
"@snowballtools/types": "^0.2.0",
|
||||||
@ -43,8 +42,8 @@
|
|||||||
"@turnkey/sdk-react": "^0.1.0",
|
"@turnkey/sdk-react": "^0.1.0",
|
||||||
"@turnkey/webauthn-stamper": "^0.5.0",
|
"@turnkey/webauthn-stamper": "^0.5.0",
|
||||||
"@walletconnect/ethereum-provider": "^2.12.2",
|
"@walletconnect/ethereum-provider": "^2.12.2",
|
||||||
"@web3modal/siwe": "^4.0.5",
|
"@web3modal/siwe": "4.0.5",
|
||||||
"@web3modal/wagmi": "^4.0.5",
|
"@web3modal/wagmi": "4.0.5",
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
@ -62,15 +61,15 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropdown": "^1.11.0",
|
"react-dropdown": "^1.11.0",
|
||||||
"react-hook-form": "^7.49.0",
|
"react-hook-form": "^7.49.0",
|
||||||
"react-hot-toast": "^2.4.1",
|
|
||||||
"react-oauth-popup": "^1.0.5",
|
"react-oauth-popup": "^1.0.5",
|
||||||
"react-router-dom": "^6.20.1",
|
"react-router-dom": "^6.20.1",
|
||||||
"react-timer-hook": "^3.0.7",
|
"react-timer-hook": "^3.0.7",
|
||||||
"siwe": "^2.1.4",
|
"siwe": "2.1.4",
|
||||||
"tailwind-variants": "^0.2.0",
|
"tailwind-variants": "^0.2.0",
|
||||||
"usehooks-ts": "^2.15.1",
|
"usehooks-ts": "^2.15.1",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"viem": "^2.7.11",
|
"viem": "^2.7.11",
|
||||||
|
"wagmi": "2.5.7",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
1
packages/frontend/public/.well-known/walletconnect.txt
Normal file
1
packages/frontend/public/.well-known/walletconnect.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
350e9ac2-8b27-4a79-9a82-78cfdb68ef71=0eacb7ae462f82c8b0199d28193b0bfa5265973dbb1fe991eec2cab737dfc1ec
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
import Projects from './pages/org-slug';
|
import Projects from './pages/org-slug';
|
||||||
@ -10,8 +11,8 @@ import ProjectSearchLayout from './layouts/ProjectSearch';
|
|||||||
import Index from './pages';
|
import Index from './pages';
|
||||||
import AuthPage from './pages/AuthPage';
|
import AuthPage from './pages/AuthPage';
|
||||||
import { DashboardLayout } from './pages/org-slug/layout';
|
import { DashboardLayout } from './pages/org-slug/layout';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import Web3Provider from 'context/Web3Provider';
|
import Web3Provider from 'context/Web3Provider';
|
||||||
|
import { BASE_URL } from 'utils/constants';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -49,25 +50,26 @@ const router = createBrowserRouter([
|
|||||||
path: '/login',
|
path: '/login',
|
||||||
element: <AuthPage />,
|
element: <AuthPage />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/signup',
|
|
||||||
element: <AuthPage />,
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// Hacky way of checking session
|
// Hacky way of checking session
|
||||||
// TODO: Handle redirect backs
|
// TODO: Handle redirect backs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`${import.meta.env.VITE_SERVER_URL}/auth/session`, {
|
fetch(`${BASE_URL}/auth/session`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
|
const path = window.location.pathname;
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
const path = window.location.pathname;
|
|
||||||
if (path !== '/login' && path !== '/signup') {
|
if (path !== '/login') {
|
||||||
window.location.pathname = '/login';
|
window.location.pathname = '/login';
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (path === '/login') {
|
||||||
|
window.location.pathname = '/';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
|
import {
|
||||||
|
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO,
|
||||||
|
VITE_GITHUB_PWA_TEMPLATE_REPO,
|
||||||
|
} from 'utils/constants';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'Progressive Web App (PWA)',
|
name: 'Progressive Web App (PWA)',
|
||||||
icon: 'pwa',
|
icon: 'pwa',
|
||||||
repoFullName: `${import.meta.env.VITE_GITHUB_PWA_TEMPLATE_REPO}`,
|
repoFullName: `${VITE_GITHUB_PWA_TEMPLATE_REPO}`,
|
||||||
isComingSoon: false,
|
isComingSoon: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
name: 'Image Upload PWA',
|
name: 'Image Upload PWA',
|
||||||
icon: 'pwa',
|
icon: 'pwa',
|
||||||
repoFullName: `${import.meta.env.VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
|
repoFullName: `${VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
|
||||||
isComingSoon: false,
|
isComingSoon: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -6,7 +6,7 @@ import { Input, InputProps } from './shared/Input';
|
|||||||
const SearchBar: React.ForwardRefRenderFunction<
|
const SearchBar: React.ForwardRefRenderFunction<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
InputProps & RefAttributes<HTMLInputElement>
|
InputProps & RefAttributes<HTMLInputElement>
|
||||||
> = ({ value, onChange, placeholder = 'Search', ...props }) => {
|
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex w-full">
|
<div className="relative flex w-full">
|
||||||
<Input
|
<Input
|
||||||
@ -18,6 +18,7 @@ const SearchBar: React.ForwardRefRenderFunction<
|
|||||||
appearance="borderless"
|
appearance="borderless"
|
||||||
className="w-full lg:w-[459px]"
|
className="w-full lg:w-[459px]"
|
||||||
{...props}
|
{...props}
|
||||||
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
import { useStopwatch } from 'react-timer-hook';
|
import { useStopwatch } from 'react-timer-hook';
|
||||||
|
|
||||||
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
|
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
|
||||||
@ -12,14 +13,19 @@ const setStopWatchOffset = (time: string) => {
|
|||||||
|
|
||||||
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
||||||
offsetTimestamp: Date;
|
offsetTimestamp: Date;
|
||||||
|
isPaused: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => {
|
const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => {
|
||||||
const { totalSeconds } = useStopwatch({
|
const { totalSeconds, pause, start } = useStopwatch({
|
||||||
autoStart: true,
|
autoStart: true,
|
||||||
offsetTimestamp: offsetTimestamp,
|
offsetTimestamp: offsetTimestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isPaused ? pause() : start();
|
||||||
|
}, [isPaused]);
|
||||||
|
|
||||||
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,11 +10,16 @@ import {
|
|||||||
LinkChainIcon,
|
LinkChainIcon,
|
||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import { TagProps } from 'components/shared/Tag';
|
import { TagProps } from 'components/shared/Tag';
|
||||||
|
import {
|
||||||
|
ArrowRightCircleFilledIcon,
|
||||||
|
LoadingIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
|
||||||
interface ChangeStateToProductionDialogProps extends ConfirmDialogProps {
|
interface ChangeStateToProductionDialogProps extends ConfirmDialogProps {
|
||||||
deployment: Deployment;
|
deployment: Deployment;
|
||||||
newDeployment?: Deployment;
|
newDeployment?: Deployment;
|
||||||
domains: Domain[];
|
domains: Domain[];
|
||||||
|
isConfirmButtonLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChangeStateToProductionDialog = ({
|
export const ChangeStateToProductionDialog = ({
|
||||||
@ -24,6 +29,7 @@ export const ChangeStateToProductionDialog = ({
|
|||||||
open,
|
open,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
handleConfirm,
|
handleConfirm,
|
||||||
|
isConfirmButtonLoading,
|
||||||
...props
|
...props
|
||||||
}: ChangeStateToProductionDialogProps) => {
|
}: ChangeStateToProductionDialogProps) => {
|
||||||
const currentChip = {
|
const currentChip = {
|
||||||
@ -41,6 +47,14 @@ export const ChangeStateToProductionDialog = ({
|
|||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
open={open}
|
open={open}
|
||||||
handleConfirm={handleConfirm}
|
handleConfirm={handleConfirm}
|
||||||
|
confirmButtonProps={{
|
||||||
|
disabled: isConfirmButtonLoading,
|
||||||
|
rightIcon: isConfirmButtonLoading ? (
|
||||||
|
<LoadingIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowRightCircleFilledIcon />
|
||||||
|
),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-7">
|
<div className="flex flex-col gap-7">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
import ConfirmDialog, {
|
||||||
|
ConfirmDialogProps,
|
||||||
|
} from 'components/shared/ConfirmDialog';
|
||||||
|
import {
|
||||||
|
ArrowRightCircleFilledIcon,
|
||||||
|
LoadingIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
|
||||||
|
interface DeleteDeploymentDialogProps extends ConfirmDialogProps {
|
||||||
|
isConfirmButtonLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteDeploymentDialog = ({
|
||||||
|
open,
|
||||||
|
handleCancel,
|
||||||
|
handleConfirm,
|
||||||
|
isConfirmButtonLoading,
|
||||||
|
...props
|
||||||
|
}: DeleteDeploymentDialogProps) => {
|
||||||
|
return (
|
||||||
|
<ConfirmDialog
|
||||||
|
{...props}
|
||||||
|
dialogTitle="Delete deployment?"
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
open={open}
|
||||||
|
confirmButtonTitle={
|
||||||
|
isConfirmButtonLoading
|
||||||
|
? 'Deleting deployment'
|
||||||
|
: 'Yes, delete deployment'
|
||||||
|
}
|
||||||
|
handleConfirm={handleConfirm}
|
||||||
|
confirmButtonProps={{
|
||||||
|
variant: 'danger',
|
||||||
|
disabled: isConfirmButtonLoading,
|
||||||
|
rightIcon: isConfirmButtonLoading ? (
|
||||||
|
<LoadingIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowRightCircleFilledIcon />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="text-sm text-elements-high-em">
|
||||||
|
Once deleted, the deployment will not be accessible.
|
||||||
|
</p>
|
||||||
|
</ConfirmDialog>
|
||||||
|
);
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useCombobox } from 'downshift';
|
import { useCombobox } from 'downshift';
|
||||||
import { Project } from 'gql-client';
|
import { Project } from 'gql-client';
|
||||||
import { useDebounce } from 'usehooks-ts';
|
import { useDebounceValue } from 'usehooks-ts';
|
||||||
|
|
||||||
import SearchBar from 'components/SearchBar';
|
import SearchBar from 'components/SearchBar';
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
@ -42,7 +42,7 @@ export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const debouncedInputValue = useDebounce<string>(inputValue, 300);
|
const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300);
|
||||||
|
|
||||||
const fetchProjects = useCallback(
|
const fetchProjects = useCallback(
|
||||||
async (inputValue: string) => {
|
async (inputValue: string) => {
|
||||||
@ -62,7 +62,7 @@ export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
|
|||||||
<div className="relative w-full lg:w-fit">
|
<div className="relative w-full lg:w-fit">
|
||||||
<SearchBar {...getInputProps()} />
|
<SearchBar {...getInputProps()} />
|
||||||
<div
|
<div
|
||||||
{...getMenuProps()}
|
{...getMenuProps({}, { suppressRefError: true })}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col shadow-dropdown rounded-xl bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
|
'flex flex-col shadow-dropdown rounded-xl bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
|
||||||
{ hidden: !inputValue || !isOpen },
|
{ hidden: !inputValue || !isOpen },
|
||||||
|
@ -5,7 +5,7 @@ import { CrossIcon, SearchIcon } from 'components/shared/CustomIcon';
|
|||||||
import { Input } from 'components/shared/Input';
|
import { Input } from 'components/shared/Input';
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
import { Project } from 'gql-client';
|
import { Project } from 'gql-client';
|
||||||
import { useDebounce } from 'usehooks-ts';
|
import { useDebounceValue } from 'usehooks-ts';
|
||||||
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
|
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
|
||||||
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
|
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@ -27,25 +27,30 @@ export const ProjectSearchBarDialog = ({
|
|||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { getInputProps, getItemProps, inputValue, setInputValue } =
|
const {
|
||||||
useCombobox({
|
getInputProps,
|
||||||
items,
|
getItemProps,
|
||||||
itemToString(item) {
|
getMenuProps,
|
||||||
return item ? item.name : '';
|
inputValue,
|
||||||
},
|
setInputValue,
|
||||||
selectedItem,
|
} = useCombobox({
|
||||||
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
|
items,
|
||||||
if (newSelectedItem) {
|
itemToString(item) {
|
||||||
setSelectedItem(newSelectedItem);
|
return item ? item.name : '';
|
||||||
onClickItem?.(newSelectedItem);
|
},
|
||||||
navigate(
|
selectedItem,
|
||||||
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`,
|
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
|
||||||
);
|
if (newSelectedItem) {
|
||||||
}
|
setSelectedItem(newSelectedItem);
|
||||||
},
|
onClickItem?.(newSelectedItem);
|
||||||
});
|
navigate(
|
||||||
|
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const debouncedInputValue = useDebounce<string>(inputValue, 300);
|
const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300);
|
||||||
|
|
||||||
const fetchProjects = useCallback(
|
const fetchProjects = useCallback(
|
||||||
async (inputValue: string) => {
|
async (inputValue: string) => {
|
||||||
@ -75,7 +80,7 @@ export const ProjectSearchBarDialog = ({
|
|||||||
<div className="h-full flex flex-col fixed top-0 inset-0">
|
<div className="h-full flex flex-col fixed top-0 inset-0">
|
||||||
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
|
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
|
||||||
<Input
|
<Input
|
||||||
{...getInputProps()}
|
{...getInputProps({}, { suppressRefError: true })}
|
||||||
leftIcon={<SearchIcon />}
|
leftIcon={<SearchIcon />}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
appearance="borderless"
|
appearance="borderless"
|
||||||
@ -86,23 +91,33 @@ export const ProjectSearchBarDialog = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex flex-col gap-1 px-2 py-2">
|
<div
|
||||||
{items.length > 0
|
className="flex flex-col gap-1 px-2 py-2"
|
||||||
? items.map((item, index) => (
|
{...getMenuProps(
|
||||||
<>
|
{},
|
||||||
<div className="px-2 py-2">
|
{
|
||||||
<p className="text-elements-mid-em text-xs font-medium">
|
suppressRefError: true,
|
||||||
Suggestions
|
},
|
||||||
</p>
|
)}
|
||||||
</div>
|
>
|
||||||
<ProjectSearchBarItem
|
{items.length > 0 ? (
|
||||||
{...getItemProps({ item, index })}
|
<>
|
||||||
key={item.id}
|
<div className="px-2 py-2">
|
||||||
item={item}
|
<p className="text-elements-mid-em text-xs font-medium">
|
||||||
/>
|
Suggestions
|
||||||
</>
|
</p>
|
||||||
))
|
</div>
|
||||||
: inputValue && <ProjectSearchBarEmpty />}
|
{items.map((item, index) => (
|
||||||
|
<ProjectSearchBarItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
{...getItemProps({ item, index })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
inputValue && <ProjectSearchBarEmpty />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
|
571
packages/frontend/src/components/projects/create/Configure.tsx
Normal file
571
packages/frontend/src/components/projects/create/Configure.tsx
Normal file
@ -0,0 +1,571 @@
|
|||||||
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import { FormProvider, FieldValues } from 'react-hook-form';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { useMediaQuery } from 'usehooks-ts';
|
||||||
|
import {
|
||||||
|
AddEnvironmentVariableInput,
|
||||||
|
AuctionParams,
|
||||||
|
Deployer,
|
||||||
|
} from 'gql-client';
|
||||||
|
|
||||||
|
import { Select, MenuItem, FormControl, FormHelperText } from '@mui/material';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowRightCircleFilledIcon,
|
||||||
|
LoadingIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { Heading } from '../../shared/Heading';
|
||||||
|
import { Button } from '../../shared/Button';
|
||||||
|
import { Input } from 'components/shared/Input';
|
||||||
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
|
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
|
||||||
|
import { EnvironmentVariablesFormValues } from 'types/types';
|
||||||
|
import ConnectWallet from './ConnectWallet';
|
||||||
|
import { useWalletConnectClient } from 'context/WalletConnectContext';
|
||||||
|
|
||||||
|
type ConfigureDeploymentFormValues = {
|
||||||
|
option: string;
|
||||||
|
lrn?: string;
|
||||||
|
numProviders?: number;
|
||||||
|
maxPrice?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConfigureFormValues = ConfigureDeploymentFormValues &
|
||||||
|
EnvironmentVariablesFormValues;
|
||||||
|
|
||||||
|
const DEFAULT_MAX_PRICE = '10000';
|
||||||
|
|
||||||
|
const Configure = () => {
|
||||||
|
const { signClient, session, accounts } = useWalletConnectClient();
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [deployers, setDeployers] = useState<Deployer[]>([]);
|
||||||
|
const [selectedAccount, setSelectedAccount] = useState<string>();
|
||||||
|
const [selectedDeployer, setSelectedDeployer] = useState<Deployer>();
|
||||||
|
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
|
||||||
|
const [isPaymentDone, setIsPaymentDone] = useState(false);
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const templateId = searchParams.get('templateId');
|
||||||
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
const owner = queryParams.get('owner');
|
||||||
|
const name = queryParams.get('name');
|
||||||
|
const defaultBranch = queryParams.get('defaultBranch');
|
||||||
|
const fullName = queryParams.get('fullName');
|
||||||
|
const orgSlug = queryParams.get('orgSlug');
|
||||||
|
const templateOwner = queryParams.get('templateOwner');
|
||||||
|
const templateRepo = queryParams.get('templateRepo');
|
||||||
|
const isPrivate = queryParams.get('isPrivate') === 'true';
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { toast, dismiss } = useToast();
|
||||||
|
const client = useGQLClient();
|
||||||
|
|
||||||
|
const methods = useForm<ConfigureFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
option: 'Auction',
|
||||||
|
maxPrice: DEFAULT_MAX_PRICE,
|
||||||
|
lrn: '',
|
||||||
|
numProviders: 1,
|
||||||
|
variables: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedOption = methods.watch('option');
|
||||||
|
|
||||||
|
const isTabletView = useMediaQuery('(min-width: 720px)'); // md:
|
||||||
|
const buttonSize = isTabletView ? { size: 'lg' as const } : {};
|
||||||
|
|
||||||
|
const createProject = async (
|
||||||
|
data: FieldValues,
|
||||||
|
envVariables: AddEnvironmentVariableInput[],
|
||||||
|
senderAddress: string,
|
||||||
|
txHash: string,
|
||||||
|
): Promise<string> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
let projectId: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let lrn: string | undefined;
|
||||||
|
let auctionParams: AuctionParams | undefined;
|
||||||
|
|
||||||
|
if (data.option === 'LRN') {
|
||||||
|
lrn = data.lrn;
|
||||||
|
} else if (data.option === 'Auction') {
|
||||||
|
auctionParams = {
|
||||||
|
numProviders: Number(data.numProviders!),
|
||||||
|
maxPrice: data.maxPrice!.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templateId) {
|
||||||
|
const projectData: any = {
|
||||||
|
templateOwner,
|
||||||
|
templateRepo,
|
||||||
|
owner,
|
||||||
|
name,
|
||||||
|
isPrivate,
|
||||||
|
paymentAddress: senderAddress,
|
||||||
|
txHash,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { addProjectFromTemplate } = await client.addProjectFromTemplate(
|
||||||
|
orgSlug!,
|
||||||
|
projectData,
|
||||||
|
lrn,
|
||||||
|
auctionParams,
|
||||||
|
envVariables,
|
||||||
|
);
|
||||||
|
|
||||||
|
projectId = addProjectFromTemplate.id;
|
||||||
|
} else {
|
||||||
|
const { addProject } = await client.addProject(
|
||||||
|
orgSlug!,
|
||||||
|
{
|
||||||
|
name: `${owner}-${name}`,
|
||||||
|
prodBranch: defaultBranch!,
|
||||||
|
repository: fullName!,
|
||||||
|
template: 'webapp',
|
||||||
|
paymentAddress: senderAddress,
|
||||||
|
txHash,
|
||||||
|
},
|
||||||
|
lrn,
|
||||||
|
auctionParams,
|
||||||
|
envVariables,
|
||||||
|
);
|
||||||
|
|
||||||
|
projectId = addProject.id;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating project:', error);
|
||||||
|
toast({
|
||||||
|
id: 'error-creating-project',
|
||||||
|
title: 'Error creating project',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectId) {
|
||||||
|
return projectId;
|
||||||
|
} else {
|
||||||
|
throw new Error('Project creation failed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyTx = async (
|
||||||
|
senderAddress: string,
|
||||||
|
txHash: string,
|
||||||
|
amount: string,
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const isValid = await client.verifyTx(
|
||||||
|
txHash,
|
||||||
|
`${amount.toString()}alnt`,
|
||||||
|
senderAddress,
|
||||||
|
);
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = useCallback(
|
||||||
|
async (createFormData: FieldValues) => {
|
||||||
|
try {
|
||||||
|
const deployerLrn = createFormData.lrn;
|
||||||
|
const deployer = deployers.find(
|
||||||
|
(deployer) => deployer.deployerLrn === deployerLrn,
|
||||||
|
);
|
||||||
|
|
||||||
|
let amount: string;
|
||||||
|
let senderAddress: string;
|
||||||
|
let txHash: string;
|
||||||
|
if (createFormData.option === 'LRN' && !deployer?.minimumPayment) {
|
||||||
|
toast({
|
||||||
|
id: 'no-payment-required',
|
||||||
|
title: 'No payment required. Deploying app...',
|
||||||
|
variant: 'info',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
|
||||||
|
txHash = '';
|
||||||
|
senderAddress = '';
|
||||||
|
} else {
|
||||||
|
if (!selectedAccount) return;
|
||||||
|
|
||||||
|
senderAddress = selectedAccount.split(':')[2];
|
||||||
|
|
||||||
|
if (createFormData.option === 'LRN') {
|
||||||
|
amount = deployer?.minimumPayment!;
|
||||||
|
} else {
|
||||||
|
amount = (
|
||||||
|
createFormData.numProviders * createFormData.maxPrice
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const amountToBePaid = amount.replace(/\D/g, '').toString();
|
||||||
|
|
||||||
|
const txHashResponse = await cosmosSendTokensHandler(
|
||||||
|
selectedAccount,
|
||||||
|
amountToBePaid,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!txHashResponse) {
|
||||||
|
console.error('Tx not successful');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
txHash = txHashResponse;
|
||||||
|
|
||||||
|
const isTxHashValid = await verifyTx(
|
||||||
|
senderAddress,
|
||||||
|
txHash,
|
||||||
|
amountToBePaid.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isTxHashValid === false) {
|
||||||
|
console.error('Invalid Tx hash', txHash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const environmentVariables = createFormData.variables.map(
|
||||||
|
(variable: any) => {
|
||||||
|
return {
|
||||||
|
key: variable.key,
|
||||||
|
value: variable.value,
|
||||||
|
environments: Object.entries(createFormData.environment)
|
||||||
|
.filter(([, value]) => value === true)
|
||||||
|
.map(([key]) => key.charAt(0).toUpperCase() + key.slice(1)),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectId = await createProject(
|
||||||
|
createFormData,
|
||||||
|
environmentVariables,
|
||||||
|
senderAddress,
|
||||||
|
txHash,
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.getEnvironmentVariables(projectId);
|
||||||
|
|
||||||
|
if (templateId) {
|
||||||
|
createFormData.option === 'Auction'
|
||||||
|
? navigate(
|
||||||
|
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
|
||||||
|
)
|
||||||
|
: navigate(
|
||||||
|
`/${orgSlug}/projects/create/template/deploy?projectId=${projectId}&templateId=${templateId}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
createFormData.option === 'Auction'
|
||||||
|
? navigate(
|
||||||
|
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
|
||||||
|
)
|
||||||
|
: navigate(
|
||||||
|
`/${orgSlug}/projects/create/deploy?projectId=${projectId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast({
|
||||||
|
id: 'error-deploying-app',
|
||||||
|
title: 'Error deploying app',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client, createProject, dismiss, toast],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchDeployers = useCallback(async () => {
|
||||||
|
const res = await client.getDeployers();
|
||||||
|
setDeployers(res.deployers);
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
|
const onAccountChange = useCallback((account: string) => {
|
||||||
|
setSelectedAccount(account);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onDeployerChange = useCallback(
|
||||||
|
(selectedLrn: string) => {
|
||||||
|
const deployer = deployers.find((d) => d.deployerLrn === selectedLrn);
|
||||||
|
setSelectedDeployer(deployer);
|
||||||
|
},
|
||||||
|
[deployers],
|
||||||
|
);
|
||||||
|
|
||||||
|
const cosmosSendTokensHandler = useCallback(
|
||||||
|
async (selectedAccount: string, amount: string) => {
|
||||||
|
if (!signClient || !session || !selectedAccount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chainId = selectedAccount.split(':')[1];
|
||||||
|
const senderAddress = selectedAccount.split(':')[2];
|
||||||
|
const snowballAddress = await client.getAddress();
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsPaymentDone(false);
|
||||||
|
setIsPaymentLoading(true);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
id: 'sending-payment-request',
|
||||||
|
title: 'Check your wallet and approve payment request',
|
||||||
|
variant: 'loading',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: { signature: string } = await signClient.request({
|
||||||
|
topic: session.topic,
|
||||||
|
chainId: `cosmos:${chainId}`,
|
||||||
|
request: {
|
||||||
|
method: 'cosmos_sendTokens',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
from: senderAddress,
|
||||||
|
to: snowballAddress,
|
||||||
|
value: amount,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Error completing transaction');
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
id: 'payment-successful',
|
||||||
|
title: 'Payment successful',
|
||||||
|
variant: 'success',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsPaymentDone(true);
|
||||||
|
|
||||||
|
return result.signature;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error sending tokens', error);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
id: 'error-sending-tokens',
|
||||||
|
title: 'Error sending tokens',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsPaymentDone(false);
|
||||||
|
} finally {
|
||||||
|
setIsPaymentLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[session, signClient, toast],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDeployers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-7 px-4 py-6">
|
||||||
|
<div className="flex justify-between mb-6">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Heading as="h4" className="md:text-lg font-medium">
|
||||||
|
Configure deployment
|
||||||
|
</Heading>
|
||||||
|
<Heading as="h5" className="text-sm font-sans text-elements-low-em">
|
||||||
|
The app can be deployed by setting the deployer LRN for a single
|
||||||
|
deployment or by creating a deployer auction for multiple
|
||||||
|
deployments
|
||||||
|
</Heading>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-6 lg:gap-8 w-full">
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form onSubmit={methods.handleSubmit(handleFormSubmit)}>
|
||||||
|
<div className="flex flex-col justify-start gap-4 mb-6">
|
||||||
|
<Controller
|
||||||
|
name="option"
|
||||||
|
control={methods.control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
size="small"
|
||||||
|
displayEmpty
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
'& .MuiOutlinedInput-notchedOutline': {
|
||||||
|
borderColor: '#e0e0e0',
|
||||||
|
borderRadius: '8px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value="Auction">Create Auction</MenuItem>
|
||||||
|
<MenuItem value="LRN">Deployer LRN</MenuItem>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedOption === 'LRN' && (
|
||||||
|
<div className="flex flex-col justify-start gap-4 mb-6">
|
||||||
|
<Heading
|
||||||
|
as="h5"
|
||||||
|
className="text-sm font-sans text-elements-low-em"
|
||||||
|
>
|
||||||
|
The app will be deployed by the configured deployer
|
||||||
|
</Heading>
|
||||||
|
<Controller
|
||||||
|
name="lrn"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field: { value, onChange }, fieldState }) => (
|
||||||
|
<FormControl fullWidth error={Boolean(fieldState.error)}>
|
||||||
|
<span className="text-sm text-elements-high-em mb-4">
|
||||||
|
Select deployer LRN
|
||||||
|
</span>
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => {
|
||||||
|
onChange(event.target.value);
|
||||||
|
onDeployerChange(event.target.value);
|
||||||
|
}}
|
||||||
|
displayEmpty
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{deployers.map((deployer) => (
|
||||||
|
<MenuItem
|
||||||
|
key={deployer.deployerLrn}
|
||||||
|
value={deployer.deployerLrn}
|
||||||
|
>
|
||||||
|
{`${deployer.deployerLrn} ${deployer.minimumPayment ? `(${deployer.minimumPayment})` : ''}`}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{fieldState.error && (
|
||||||
|
<FormHelperText>
|
||||||
|
{fieldState.error.message}
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedOption === 'Auction' && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col justify-start gap-4 mb-6">
|
||||||
|
<Heading
|
||||||
|
as="h5"
|
||||||
|
className="text-sm font-sans text-elements-low-em"
|
||||||
|
>
|
||||||
|
Set the number of deployers and maximum price for each
|
||||||
|
deployment
|
||||||
|
</Heading>
|
||||||
|
<span className="text-sm text-elements-high-em">
|
||||||
|
Number of Deployers
|
||||||
|
</span>
|
||||||
|
<Controller
|
||||||
|
name="numProviders"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-start gap-4 mb-6">
|
||||||
|
<span className="text-sm text-elements-high-em">
|
||||||
|
Maximum Price (alnt)
|
||||||
|
</span>
|
||||||
|
<Controller
|
||||||
|
name="maxPrice"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Input type="number" value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Heading as="h4" className="md:text-lg font-medium mb-3">
|
||||||
|
Environment Variables
|
||||||
|
</Heading>
|
||||||
|
<div className="p-4 bg-slate-100 rounded-lg mb-6">
|
||||||
|
<EnvironmentVariablesForm />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedOption === 'LRN' && !selectedDeployer?.minimumPayment ? (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
{...buttonSize}
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || !selectedDeployer || !selectedAccount}
|
||||||
|
rightIcon={
|
||||||
|
isLoading ? (
|
||||||
|
<LoadingIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowRightCircleFilledIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Deploying' : 'Deploy'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Heading as="h4" className="md:text-lg font-medium mb-3">
|
||||||
|
Connect to your wallet
|
||||||
|
</Heading>
|
||||||
|
<ConnectWallet onAccountChange={onAccountChange} />
|
||||||
|
{accounts && accounts?.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
{...buttonSize}
|
||||||
|
type="submit"
|
||||||
|
disabled={
|
||||||
|
isLoading || isPaymentLoading || !selectedAccount
|
||||||
|
}
|
||||||
|
rightIcon={
|
||||||
|
isLoading || isPaymentLoading ? (
|
||||||
|
<LoadingIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowRightCircleFilledIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!isPaymentDone
|
||||||
|
? isPaymentLoading
|
||||||
|
? 'Transaction Requested'
|
||||||
|
: 'Pay and Deploy'
|
||||||
|
: isLoading
|
||||||
|
? 'Deploying'
|
||||||
|
: 'Deploy'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Configure;
|
@ -14,11 +14,10 @@ import { useToast } from '../../shared/Toast';
|
|||||||
import { IconWithFrame } from '../../shared/IconWithFrame';
|
import { IconWithFrame } from '../../shared/IconWithFrame';
|
||||||
import { Heading } from '../../shared/Heading';
|
import { Heading } from '../../shared/Heading';
|
||||||
import { MockConnectGitCard } from './MockConnectGitCard';
|
import { MockConnectGitCard } from './MockConnectGitCard';
|
||||||
|
import { VITE_GITHUB_CLIENT_ID } from 'utils/constants';
|
||||||
|
|
||||||
const SCOPES = 'repo user';
|
const SCOPES = 'repo user';
|
||||||
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
|
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`;
|
||||||
import.meta.env.VITE_GITHUB_CLIENT_ID
|
|
||||||
}&scope=${encodeURIComponent(SCOPES)}`;
|
|
||||||
|
|
||||||
interface ConnectAccountInterface {
|
interface ConnectAccountInterface {
|
||||||
onAuth: (token: string) => void;
|
onAuth: (token: string) => void;
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
import { Select, Option } from '@snowballtools/material-tailwind-react-fork';
|
||||||
|
|
||||||
|
import { Button } from '../../shared/Button';
|
||||||
|
import { useWalletConnectClient } from 'context/WalletConnectContext';
|
||||||
|
|
||||||
|
const ConnectWallet = ({
|
||||||
|
onAccountChange,
|
||||||
|
}: {
|
||||||
|
onAccountChange: (selectedAccount: string) => void;
|
||||||
|
}) => {
|
||||||
|
const { onConnect, accounts } = useWalletConnectClient();
|
||||||
|
|
||||||
|
const handleConnect = async () => {
|
||||||
|
await onConnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-slate-100 rounded-lg mb-6">
|
||||||
|
{!accounts ? (
|
||||||
|
<div>
|
||||||
|
<Button type={'button'} onClick={handleConnect}>
|
||||||
|
Connect Wallet
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
label="Select Account"
|
||||||
|
defaultValue={accounts[0].address}
|
||||||
|
onChange={(value) => {
|
||||||
|
value && onAccountChange(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{accounts.map((account, index) => (
|
||||||
|
<Option key={index} value={account.address}>
|
||||||
|
{account.address.split(':').slice(1).join(':')}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConnectWallet;
|
@ -1,5 +1,7 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Deployment } from 'gql-client';
|
||||||
|
|
||||||
import { DeployStep, DeployStatus } from './DeployStep';
|
import { DeployStep, DeployStatus } from './DeployStep';
|
||||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||||
@ -7,13 +9,37 @@ import { Heading } from '../../shared/Heading';
|
|||||||
import { Button } from '../../shared/Button';
|
import { Button } from '../../shared/Button';
|
||||||
import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon';
|
import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon';
|
||||||
import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog';
|
import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog';
|
||||||
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
|
|
||||||
|
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
|
||||||
|
|
||||||
|
type RequestState =
|
||||||
|
| 'SUBMITTED'
|
||||||
|
| 'DEPLOYING'
|
||||||
|
| 'DEPLOYED'
|
||||||
|
| 'REMOVED'
|
||||||
|
| 'CANCELLED'
|
||||||
|
| 'ERROR';
|
||||||
|
|
||||||
|
type Record = {
|
||||||
|
id: string;
|
||||||
|
createTime: string;
|
||||||
|
app: string;
|
||||||
|
lastState: RequestState;
|
||||||
|
lastUpdate: string;
|
||||||
|
logAvailable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const TIMEOUT_DURATION = 5000;
|
|
||||||
const Deploy = () => {
|
const Deploy = () => {
|
||||||
|
const client = useGQLClient();
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const projectId = searchParams.get('projectId');
|
const projectId = searchParams.get('projectId');
|
||||||
|
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const [deployment, setDeployment] = useState<Deployment>();
|
||||||
|
const [record, setRecord] = useState<Record>();
|
||||||
|
|
||||||
const handleOpen = () => setOpen(!open);
|
const handleOpen = () => setOpen(!open);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -23,13 +49,67 @@ const Deploy = () => {
|
|||||||
navigate(`/${orgSlug}/projects/create`);
|
navigate(`/${orgSlug}/projects/create`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const isDeploymentFailed = useMemo(() => {
|
||||||
const timerID = setTimeout(() => {
|
if (!record) {
|
||||||
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
return false;
|
||||||
}, TIMEOUT_DURATION);
|
}
|
||||||
|
|
||||||
return () => clearInterval(timerID);
|
// Not checking for `REMOVED` status as this status is received for a brief period before receiving `DEPLOYED` status
|
||||||
}, []);
|
if (record.lastState === 'CANCELLED' || record.lastState === 'ERROR') {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [record]);
|
||||||
|
|
||||||
|
const fetchDeploymentRecords = useCallback(async () => {
|
||||||
|
if (!deployment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const record: Record = response.data;
|
||||||
|
setRecord(record);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.log('Error fetching data from deployer', err);
|
||||||
|
}
|
||||||
|
}, [deployment]);
|
||||||
|
|
||||||
|
const fetchDeployment = useCallback(async () => {
|
||||||
|
if (!projectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { deployments } = await client.getDeployments(projectId);
|
||||||
|
setDeployment(deployments[0]);
|
||||||
|
}, [client, projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDeployment();
|
||||||
|
fetchDeploymentRecords();
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchDeploymentRecords();
|
||||||
|
}, FETCH_DEPLOYMENTS_INTERVAL);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [fetchDeployment, fetchDeploymentRecords]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.lastState === 'DEPLOYED') {
|
||||||
|
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
||||||
|
}
|
||||||
|
}, [record]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-7">
|
<div className="space-y-7">
|
||||||
@ -42,6 +122,7 @@ const Deploy = () => {
|
|||||||
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
|
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
|
||||||
<Stopwatch
|
<Stopwatch
|
||||||
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
||||||
|
isPaused={isDeploymentFailed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -60,30 +141,36 @@ const Deploy = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{!isDeploymentFailed ? (
|
||||||
<DeployStep
|
<div>
|
||||||
title="Building"
|
<DeployStep
|
||||||
status={DeployStatus.COMPLETE}
|
title={record ? 'Submitted' : 'Submitting'}
|
||||||
step="1"
|
status={record ? DeployStatus.COMPLETE : DeployStatus.PROCESSING}
|
||||||
processTime="72000"
|
step="1"
|
||||||
/>
|
/>
|
||||||
<DeployStep
|
|
||||||
title="Deployment summary"
|
<DeployStep
|
||||||
status={DeployStatus.PROCESSING}
|
title={
|
||||||
step="2"
|
record && record.lastState === 'DEPLOYED'
|
||||||
startTime={Date.now().toString()}
|
? 'Deployed'
|
||||||
/>
|
: 'Deploying'
|
||||||
<DeployStep
|
}
|
||||||
title="Running checks"
|
status={
|
||||||
status={DeployStatus.NOT_STARTED}
|
!record
|
||||||
step="3"
|
? DeployStatus.NOT_STARTED
|
||||||
/>
|
: record.lastState === 'DEPLOYED'
|
||||||
<DeployStep
|
? DeployStatus.COMPLETE
|
||||||
title="Assigning domains"
|
: DeployStatus.PROCESSING
|
||||||
status={DeployStatus.NOT_STARTED}
|
}
|
||||||
step="4"
|
step="2"
|
||||||
/>
|
startTime={Date.now().toString()}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<DeployStep title={record!.lastState} status={DeployStatus.ERROR} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,27 +1,16 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
|
|
||||||
|
|
||||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||||
import FormatMillisecond from '../../FormatMilliSecond';
|
|
||||||
import processLogs from '../../../assets/process-logs.json';
|
|
||||||
import { cn } from 'utils/classnames';
|
import { cn } from 'utils/classnames';
|
||||||
import {
|
import {
|
||||||
CheckRoundFilledIcon,
|
CheckRoundFilledIcon,
|
||||||
ClockOutlineIcon,
|
ClockOutlineIcon,
|
||||||
CopyIcon,
|
|
||||||
LoaderIcon,
|
LoaderIcon,
|
||||||
MinusCircleIcon,
|
|
||||||
PlusIcon,
|
|
||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import { Button } from 'components/shared/Button';
|
|
||||||
import { useToast } from 'components/shared/Toast';
|
|
||||||
import { useIntersectionObserver } from 'usehooks-ts';
|
|
||||||
|
|
||||||
enum DeployStatus {
|
enum DeployStatus {
|
||||||
PROCESSING = 'progress',
|
PROCESSING = 'progress',
|
||||||
COMPLETE = 'complete',
|
COMPLETE = 'complete',
|
||||||
NOT_STARTED = 'notStarted',
|
NOT_STARTED = 'notStarted',
|
||||||
|
ERROR = 'error',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeployStepsProps {
|
interface DeployStepsProps {
|
||||||
@ -32,35 +21,11 @@ interface DeployStepsProps {
|
|||||||
processTime?: string;
|
processTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeployStep = ({
|
const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
|
||||||
step,
|
|
||||||
status,
|
|
||||||
title,
|
|
||||||
startTime,
|
|
||||||
processTime,
|
|
||||||
}: DeployStepsProps) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { toast, dismiss } = useToast();
|
|
||||||
const { isIntersecting: hideGradientOverlay, ref } = useIntersectionObserver({
|
|
||||||
threshold: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const disableCollapse = status !== DeployStatus.COMPLETE;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border-separator">
|
<div className="border-b border-border-separator">
|
||||||
{/* Collapisble trigger */}
|
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn('flex justify-between w-full py-5 gap-2', 'cursor-auto')}
|
||||||
'flex justify-between w-full py-5 gap-2',
|
|
||||||
disableCollapse && 'cursor-auto',
|
|
||||||
)}
|
|
||||||
tabIndex={disableCollapse ? -1 : undefined}
|
|
||||||
onClick={() => {
|
|
||||||
if (!disableCollapse) {
|
|
||||||
setIsOpen((val) => !val);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className={cn('grow flex items-center gap-3')}>
|
<div className={cn('grow flex items-center gap-3')}>
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
@ -73,12 +38,6 @@ const DeployStep = ({
|
|||||||
{status === DeployStatus.PROCESSING && (
|
{status === DeployStatus.PROCESSING && (
|
||||||
<LoaderIcon className="animate-spin text-elements-link" />
|
<LoaderIcon className="animate-spin text-elements-link" />
|
||||||
)}
|
)}
|
||||||
{status === DeployStatus.COMPLETE && (
|
|
||||||
<div className="text-controls-primary">
|
|
||||||
{!isOpen && <PlusIcon size={24} />}
|
|
||||||
{isOpen && <MinusCircleIcon size={24} />}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
@ -96,7 +55,10 @@ const DeployStep = ({
|
|||||||
{status === DeployStatus.PROCESSING && (
|
{status === DeployStatus.PROCESSING && (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<ClockOutlineIcon size={16} className="text-elements-low-em" />
|
<ClockOutlineIcon size={16} className="text-elements-low-em" />
|
||||||
<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
<Stopwatch
|
||||||
|
offsetTimestamp={setStopWatchOffset(startTime!)}
|
||||||
|
isPaused={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{status === DeployStatus.COMPLETE && (
|
{status === DeployStatus.COMPLETE && (
|
||||||
@ -107,51 +69,9 @@ const DeployStep = ({
|
|||||||
size={15}
|
size={15}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormatMillisecond time={Number(processTime)} />{' '}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Collapsible */}
|
|
||||||
<Collapse open={isOpen}>
|
|
||||||
<div className="relative text-xs text-elements-low-em h-36 overflow-y-auto">
|
|
||||||
{/* Logs */}
|
|
||||||
{processLogs.map((log, key) => {
|
|
||||||
return (
|
|
||||||
<p className="font-mono" key={key}>
|
|
||||||
{log}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* End of logs ref used for hiding gradient overlay */}
|
|
||||||
<div ref={ref} />
|
|
||||||
|
|
||||||
{/* Overflow gradient overlay */}
|
|
||||||
{!hideGradientOverlay && (
|
|
||||||
<div className="h-14 w-full sticky bottom-0 inset-x-0 bg-gradient-to-t from-white to-transparent" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Copy log button */}
|
|
||||||
<div className={cn('sticky bottom-4 left-1/2 flex justify-center')}>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(processLogs.join('\n'));
|
|
||||||
toast({
|
|
||||||
title: 'Logs copied',
|
|
||||||
variant: 'success',
|
|
||||||
id: 'logs',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
leftIcon={<CopyIcon size={16} />}
|
|
||||||
>
|
|
||||||
Copy log
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Collapse>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -38,36 +38,9 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
navigate(
|
||||||
setIsLoading(true);
|
`configure?owner=${repository.owner?.login}&name=${repository.name}&defaultBranch=${repository.default_branch}&fullName=${repository.full_name}&orgSlug=${orgSlug}`,
|
||||||
const { addProject } = await client.addProject(orgSlug, {
|
);
|
||||||
name: `${repository.owner?.login}-${repository.name}`,
|
|
||||||
prodBranch: repository.default_branch as string,
|
|
||||||
repository: repository.full_name,
|
|
||||||
// TODO: Compute template from repo
|
|
||||||
template: 'webapp',
|
|
||||||
});
|
|
||||||
if (addProject) {
|
|
||||||
navigate(`import?projectId=${addProject.id}`);
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
id: 'failed-to-create-project',
|
|
||||||
title: 'Failed to create project',
|
|
||||||
variant: 'error',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error((error as Error).message);
|
|
||||||
toast({
|
|
||||||
id: 'failed-to-create-project',
|
|
||||||
title: 'Failed to create project',
|
|
||||||
variant: 'error',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [client, repository, orgSlug, setIsLoading, navigate, toast]);
|
}, [client, repository, orgSlug, setIsLoading, navigate, toast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -64,9 +64,9 @@ export const RepositoryList = () => {
|
|||||||
|
|
||||||
// Check if selected account is an organization
|
// Check if selected account is an organization
|
||||||
if (selectedAccount.value === gitUser.login) {
|
if (selectedAccount.value === gitUser.login) {
|
||||||
query = query + ` user:${selectedAccount}`;
|
query = query + ` user:${selectedAccount.value}`;
|
||||||
} else {
|
} else {
|
||||||
query = query + ` org:${selectedAccount}`;
|
query = query + ` org:${selectedAccount.value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await octokit.rest.search.repos({
|
const result = await octokit.rest.search.repos({
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import { CopyBlock, atomOneLight } from 'react-code-blocks';
|
import { CopyBlock, atomOneLight } from 'react-code-blocks';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import { Modal } from 'components/shared/Modal';
|
||||||
Button,
|
import { Button } from 'components/shared/Button';
|
||||||
Dialog,
|
|
||||||
DialogHeader,
|
|
||||||
DialogBody,
|
|
||||||
DialogFooter,
|
|
||||||
} from '@snowballtools/material-tailwind-react-fork';
|
|
||||||
|
|
||||||
interface AssignDomainProps {
|
interface AssignDomainProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -16,36 +11,31 @@ interface AssignDomainProps {
|
|||||||
|
|
||||||
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} handler={handleOpen}>
|
<Modal open={open} onOpenChange={handleOpen}>
|
||||||
<DialogHeader>Assign Domain</DialogHeader>
|
<Modal.Content>
|
||||||
<DialogBody>
|
<Modal.Header>Assign Domain</Modal.Header>
|
||||||
In order to assign a domain to your production deployments, configure it
|
<Modal.Body>
|
||||||
in the{' '}
|
In order to assign a domain to your production deployments, configure
|
||||||
{/* TODO: Fix selection of project settings tab on navigation to domains */}
|
it in the{' '}
|
||||||
<Link to="../settings/domains" className="text-light-blue-800 inline">
|
{/* TODO: Fix selection of project settings tab on navigation to domains */}
|
||||||
project settings{' '}
|
<Link to="../settings/domains" className="text-light-blue-800 inline">
|
||||||
</Link>
|
project settings{' '}
|
||||||
(recommended). If you want to assign to this specific deployment,
|
</Link>
|
||||||
however, you can do so using our command-line interface:
|
(recommended). If you want to assign to this specific deployment,
|
||||||
{/* https://github.com/rajinwonderland/react-code-blocks/issues/138 */}
|
however, you can do so using our command-line interface:
|
||||||
<CopyBlock
|
{/* https://github.com/rajinwonderland/react-code-blocks/issues/138 */}
|
||||||
text="snowball alias <deployment> <domain>"
|
<CopyBlock
|
||||||
language=""
|
text="snowball alias <deployment> <domain>"
|
||||||
showLineNumbers={false}
|
language=""
|
||||||
theme={atomOneLight}
|
showLineNumbers={false}
|
||||||
/>
|
theme={atomOneLight}
|
||||||
</DialogBody>
|
/>
|
||||||
<DialogFooter className="flex justify-start">
|
</Modal.Body>
|
||||||
<Button
|
<Modal.Footer className="flex justify-start">
|
||||||
className="rounded-3xl"
|
<Button onClick={handleOpen}>Okay</Button>
|
||||||
variant="gradient"
|
</Modal.Footer>
|
||||||
color="blue"
|
</Modal.Content>
|
||||||
onClick={handleOpen}
|
</Modal>
|
||||||
>
|
|
||||||
<span>Okay</span>
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Dialog>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Deployment,
|
Deployment,
|
||||||
DeploymentStatus,
|
DeploymentStatus,
|
||||||
@ -6,6 +6,15 @@ import {
|
|||||||
Environment,
|
Environment,
|
||||||
Project,
|
Project,
|
||||||
} from 'gql-client';
|
} from 'gql-client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
import { Avatar } from 'components/shared/Avatar';
|
import { Avatar } from 'components/shared/Avatar';
|
||||||
import {
|
import {
|
||||||
BranchStrokeIcon,
|
BranchStrokeIcon,
|
||||||
@ -18,12 +27,23 @@ import {
|
|||||||
import { Heading } from 'components/shared/Heading';
|
import { Heading } from 'components/shared/Heading';
|
||||||
import { OverflownText } from 'components/shared/OverflownText';
|
import { OverflownText } from 'components/shared/OverflownText';
|
||||||
import { Tag, TagTheme } from 'components/shared/Tag';
|
import { Tag, TagTheme } from 'components/shared/Tag';
|
||||||
|
import { Button } from 'components/shared/Button';
|
||||||
import { getInitials } from 'utils/geInitials';
|
import { getInitials } from 'utils/geInitials';
|
||||||
import { relativeTimeMs } from 'utils/time';
|
import { relativeTimeMs } from 'utils/time';
|
||||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||||
import { formatAddress } from '../../../../utils/format';
|
import { formatAddress } from '../../../../utils/format';
|
||||||
import { DeploymentMenu } from './DeploymentMenu';
|
import { DeploymentMenu } from './DeploymentMenu';
|
||||||
|
|
||||||
|
const DEPLOYMENT_LOGS_STYLE = {
|
||||||
|
backgroundColor: 'rgba(0,0,0, .9)',
|
||||||
|
padding: '2em',
|
||||||
|
borderRadius: '0.5em',
|
||||||
|
marginLeft: '0.5em',
|
||||||
|
marginRight: '0.5em',
|
||||||
|
color: 'gray',
|
||||||
|
fontSize: 'small',
|
||||||
|
};
|
||||||
|
|
||||||
interface DeployDetailsCardProps {
|
interface DeployDetailsCardProps {
|
||||||
deployment: Deployment;
|
deployment: Deployment;
|
||||||
currentDeployment: Deployment;
|
currentDeployment: Deployment;
|
||||||
@ -48,6 +68,14 @@ const DeploymentDetailsCard = ({
|
|||||||
project,
|
project,
|
||||||
prodBranchDomains,
|
prodBranchDomains,
|
||||||
}: DeployDetailsCardProps) => {
|
}: DeployDetailsCardProps) => {
|
||||||
|
const [openDialog, setOpenDialog] = useState<boolean>(false);
|
||||||
|
const [deploymentLogs, setDeploymentLogs] = useState<string>(
|
||||||
|
'No deployment logs available',
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpenDialog = () => setOpenDialog(true);
|
||||||
|
const handleCloseDialog = () => setOpenDialog(false);
|
||||||
|
|
||||||
const getIconByDeploymentStatus = (status: DeploymentStatus) => {
|
const getIconByDeploymentStatus = (status: DeploymentStatus) => {
|
||||||
if (
|
if (
|
||||||
status === DeploymentStatus.Building ||
|
status === DeploymentStatus.Building ||
|
||||||
@ -64,18 +92,39 @@ const DeploymentDetailsCard = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchDeploymentLogs = async () => {
|
||||||
|
setDeploymentLogs('Loading logs...');
|
||||||
|
handleOpenDialog();
|
||||||
|
const statusUrl = `${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`;
|
||||||
|
const statusRes = await fetch(statusUrl, { cache: 'no-store' }).then(
|
||||||
|
(res) => res.json(),
|
||||||
|
);
|
||||||
|
if (!statusRes.logAvailable) {
|
||||||
|
setDeploymentLogs(statusRes.lastState);
|
||||||
|
} else {
|
||||||
|
const logsUrl = `${deployment.deployer.deployerApiUrl}/log/${deployment.applicationDeploymentRequestId}`;
|
||||||
|
const logsRes = await fetch(logsUrl, { cache: 'no-store' }).then((res) =>
|
||||||
|
res.text(),
|
||||||
|
);
|
||||||
|
setDeploymentLogs(logsRes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderDeploymentStatus = useCallback(
|
const renderDeploymentStatus = useCallback(
|
||||||
(className?: string) => {
|
(className?: string) => {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<Tooltip title="Click to view build logs">
|
||||||
<Tag
|
<div className={className} style={{ cursor: 'pointer' }}>
|
||||||
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
<Tag
|
||||||
size="xs"
|
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||||
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
size="xs"
|
||||||
>
|
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||||
{deployment.status}
|
onClick={fetchDeploymentLogs}
|
||||||
</Tag>
|
>
|
||||||
</div>
|
{deployment.status}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[deployment.status, deployment.commitHash],
|
[deployment.status, deployment.commitHash],
|
||||||
@ -83,7 +132,7 @@ const DeploymentDetailsCard = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative">
|
<div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative">
|
||||||
<div className="flex-1 flex justify-between w-full md:max-w-[25%] lg:max-w-[28%]">
|
<div className="flex-1 flex justify-between w-full md:max-w-[30%] lg:max-w-[33%]">
|
||||||
<div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full">
|
<div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full">
|
||||||
{/* DEPLOYMENT URL */}
|
{/* DEPLOYMENT URL */}
|
||||||
{deployment.url && (
|
{deployment.url && (
|
||||||
@ -96,7 +145,12 @@ const DeploymentDetailsCard = ({
|
|||||||
</OverflownText>
|
</OverflownText>
|
||||||
</Heading>
|
</Heading>
|
||||||
)}
|
)}
|
||||||
<span className="text-sm text-elements-low-em tracking-tight">
|
{deployment.deployer.deployerLrn && (
|
||||||
|
<span className="text-sm text-elements-low-em tracking-tight block mt-2">
|
||||||
|
Deployer LRN: {deployment.deployer.deployerLrn}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-elements-low-em tracking-tight block">
|
||||||
{deployment.environment === Environment.Production
|
{deployment.environment === Environment.Production
|
||||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||||
: 'Preview'}
|
: 'Preview'}
|
||||||
@ -162,6 +216,20 @@ const DeploymentDetailsCard = ({
|
|||||||
prodBranchDomains={prodBranchDomains}
|
prodBranchDomains={prodBranchDomains}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Dialog
|
||||||
|
open={openDialog}
|
||||||
|
onClose={handleCloseDialog}
|
||||||
|
fullWidth
|
||||||
|
maxWidth="md"
|
||||||
|
>
|
||||||
|
<DialogTitle>Deployment logs</DialogTitle>
|
||||||
|
<DialogContent style={DEPLOYMENT_LOGS_STYLE}>
|
||||||
|
{deploymentLogs && <pre>{deploymentLogs}</pre>}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleCloseDialog}>Close</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState, ComponentPropsWithRef } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuHandler,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
} from '@snowballtools/material-tailwind-react-fork';
|
||||||
|
|
||||||
import { Deployment, Domain, Environment, Project } from 'gql-client';
|
import { Deployment, Domain, Environment, Project } from 'gql-client';
|
||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import {
|
import {
|
||||||
@ -11,17 +18,12 @@ import {
|
|||||||
UndoIcon,
|
UndoIcon,
|
||||||
CrossCircleIcon,
|
CrossCircleIcon,
|
||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import {
|
|
||||||
Menu,
|
|
||||||
MenuHandler,
|
|
||||||
MenuItem,
|
|
||||||
MenuList,
|
|
||||||
} from '@snowballtools/material-tailwind-react-fork';
|
|
||||||
import { ComponentPropsWithRef } from 'react';
|
|
||||||
import AssignDomainDialog from './AssignDomainDialog';
|
import AssignDomainDialog from './AssignDomainDialog';
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
import { cn } from 'utils/classnames';
|
import { cn } from 'utils/classnames';
|
||||||
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
|
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
|
||||||
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
import { DeleteDeploymentDialog } from 'components/projects/Dialog/DeleteDeploymentDialog';
|
||||||
|
|
||||||
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
|
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
|
||||||
deployment: Deployment;
|
deployment: Deployment;
|
||||||
@ -41,29 +43,55 @@ export const DeploymentMenu = ({
|
|||||||
...props
|
...props
|
||||||
}: DeploymentMenuProps) => {
|
}: DeploymentMenuProps) => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
|
const { toast, dismiss } = useToast();
|
||||||
|
|
||||||
const [changeToProduction, setChangeToProduction] = useState(false);
|
const [changeToProduction, setChangeToProduction] = useState(false);
|
||||||
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
||||||
|
const [deleteDeploymentDialog, setDeleteDeploymentDialog] = useState(false);
|
||||||
|
const [isConfirmDeleteLoading, setIsConfirmDeleteLoading] = useState(false);
|
||||||
const [rollbackDeployment, setRollbackDeployment] = useState(false);
|
const [rollbackDeployment, setRollbackDeployment] = useState(false);
|
||||||
const [assignDomainDialog, setAssignDomainDialog] = useState(false);
|
const [assignDomainDialog, setAssignDomainDialog] = useState(false);
|
||||||
|
const [isConfirmButtonLoading, setConfirmButtonLoadingLoading] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const updateDeployment = async () => {
|
const updateDeployment = async () => {
|
||||||
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
||||||
if (isUpdated) {
|
if (isUpdated.updateDeploymentToProd) {
|
||||||
await onUpdate();
|
await onUpdate();
|
||||||
toast.success('Deployment changed to production');
|
toast({
|
||||||
|
id: 'deployment_changed_to_production',
|
||||||
|
title: 'Deployment changed to production',
|
||||||
|
variant: 'success',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.error('Unable to change deployment to production');
|
toast({
|
||||||
|
id: 'deployment_not_changed_to_production',
|
||||||
|
title: 'Error changing deployment to production',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const redeployToProd = async () => {
|
const redeployToProd = async () => {
|
||||||
const isRedeployed = await client.redeployToProd(deployment.id);
|
const isRedeployed = await client.redeployToProd(deployment.id);
|
||||||
if (isRedeployed) {
|
setConfirmButtonLoadingLoading(false);
|
||||||
|
if (isRedeployed.redeployToProd) {
|
||||||
await onUpdate();
|
await onUpdate();
|
||||||
toast.success('Redeployed to production');
|
toast({
|
||||||
|
id: 'redeployed_to_production',
|
||||||
|
title: 'Redeployed to production',
|
||||||
|
variant: 'success',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.error('Unable to redeploy to production');
|
toast({
|
||||||
|
id: 'redeployed_to_production_failed',
|
||||||
|
title: 'Error redeploying to production',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -72,21 +100,45 @@ export const DeploymentMenu = ({
|
|||||||
project.id,
|
project.id,
|
||||||
deployment.id,
|
deployment.id,
|
||||||
);
|
);
|
||||||
if (isRollbacked) {
|
if (isRollbacked.rollbackDeployment) {
|
||||||
await onUpdate();
|
await onUpdate();
|
||||||
toast.success('Deployment rolled back');
|
toast({
|
||||||
|
id: 'deployment_rolled_back',
|
||||||
|
title: 'Deployment rolled back',
|
||||||
|
variant: 'success',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.error('Unable to rollback deployment');
|
toast({
|
||||||
|
id: 'deployment_rollback_failed',
|
||||||
|
title: 'Error rolling back deployment',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteDeployment = async () => {
|
const deleteDeployment = async () => {
|
||||||
const isDeleted = await client.deleteDeployment(deployment.id);
|
const isDeleted = await client.deleteDeployment(deployment.id);
|
||||||
if (isDeleted) {
|
|
||||||
|
setIsConfirmDeleteLoading(false);
|
||||||
|
setDeleteDeploymentDialog((preVal) => !preVal);
|
||||||
|
|
||||||
|
if (isDeleted.deleteDeployment) {
|
||||||
await onUpdate();
|
await onUpdate();
|
||||||
toast.success('Deleted deployment');
|
toast({
|
||||||
|
id: 'deployment_removal_requested',
|
||||||
|
title: 'Deployment removal requested',
|
||||||
|
variant: 'success',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.error('Unable to delete deployment');
|
toast({
|
||||||
|
id: 'deployment_not_deleted',
|
||||||
|
title: 'Error deleting deployment',
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -160,7 +212,7 @@ export const DeploymentMenu = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||||
onClick={() => deleteDeployment()}
|
onClick={() => setDeleteDeploymentDialog((preVal) => !preVal)}
|
||||||
>
|
>
|
||||||
<CrossCircleIcon /> Delete deployment
|
<CrossCircleIcon /> Delete deployment
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -186,11 +238,13 @@ export const DeploymentMenu = ({
|
|||||||
open={redeployToProduction}
|
open={redeployToProduction}
|
||||||
confirmButtonTitle="Redeploy"
|
confirmButtonTitle="Redeploy"
|
||||||
handleConfirm={async () => {
|
handleConfirm={async () => {
|
||||||
|
setConfirmButtonLoadingLoading(true);
|
||||||
await redeployToProd();
|
await redeployToProd();
|
||||||
setRedeployToProduction((preVal) => !preVal);
|
setRedeployToProduction((preVal) => !preVal);
|
||||||
}}
|
}}
|
||||||
deployment={deployment}
|
deployment={deployment}
|
||||||
domains={deployment.domain ? [deployment.domain] : []}
|
domains={deployment.domain ? [deployment.domain] : []}
|
||||||
|
isConfirmButtonLoading={isConfirmButtonLoading}
|
||||||
/>
|
/>
|
||||||
{Boolean(currentDeployment) && (
|
{Boolean(currentDeployment) && (
|
||||||
<ChangeStateToProductionDialog
|
<ChangeStateToProductionDialog
|
||||||
@ -211,6 +265,15 @@ export const DeploymentMenu = ({
|
|||||||
open={assignDomainDialog}
|
open={assignDomainDialog}
|
||||||
handleOpen={() => setAssignDomainDialog(!assignDomainDialog)}
|
handleOpen={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||||
/>
|
/>
|
||||||
|
<DeleteDeploymentDialog
|
||||||
|
open={deleteDeploymentDialog}
|
||||||
|
handleConfirm={async () => {
|
||||||
|
setIsConfirmDeleteLoading(true);
|
||||||
|
await deleteDeployment();
|
||||||
|
}}
|
||||||
|
handleCancel={() => setDeleteDeploymentDialog((preVal) => !preVal)}
|
||||||
|
isConfirmButtonLoading={isConfirmDeleteLoading}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,171 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Auction, Deployer, Project } from 'gql-client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CheckRoundFilledIcon,
|
||||||
|
LoadingIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
|
import { Button, Heading, Tag } from 'components/shared';
|
||||||
|
|
||||||
|
const WAIT_DURATION = 5000;
|
||||||
|
|
||||||
|
const DIALOG_STYLE = {
|
||||||
|
backgroundColor: 'rgba(0,0,0, .9)',
|
||||||
|
padding: '2em',
|
||||||
|
borderRadius: '0.5em',
|
||||||
|
marginLeft: '0.5em',
|
||||||
|
marginRight: '0.5em',
|
||||||
|
color: 'gray',
|
||||||
|
fontSize: 'small',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuctionCard = ({ project }: { project: Project }) => {
|
||||||
|
const [auctionStatus, setAuctionStatus] = useState<string>('');
|
||||||
|
const [deployers, setDeployers] = useState<Deployer[]>([]);
|
||||||
|
const [fundsStatus, setFundsStatus] = useState<boolean>(false);
|
||||||
|
const [auctionDetails, setAuctionDetails] = useState<Auction | null>(null);
|
||||||
|
const [openDialog, setOpenDialog] = useState<boolean>(false);
|
||||||
|
const client = useGQLClient();
|
||||||
|
|
||||||
|
const getIconByAuctionStatus = (status: string) =>
|
||||||
|
status === 'completed' ? (
|
||||||
|
<CheckRoundFilledIcon />
|
||||||
|
) : (
|
||||||
|
<LoadingIcon className="animate-spin" />
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkAuctionStatus = useCallback(async () => {
|
||||||
|
const result = await client.getAuctionData(project.auctionId);
|
||||||
|
setAuctionStatus(result.status);
|
||||||
|
setAuctionDetails(result);
|
||||||
|
}, [project.auctionId, project.deployers, project.fundsReleased]);
|
||||||
|
|
||||||
|
const fetchUpdatedProject = useCallback(async () => {
|
||||||
|
const updatedProject = await client.getProject(project.id);
|
||||||
|
setDeployers(updatedProject.project!.deployers!);
|
||||||
|
setFundsStatus(updatedProject.project!.fundsReleased!);
|
||||||
|
}, [project.id]);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
await Promise.all([checkAuctionStatus(), fetchUpdatedProject()]);
|
||||||
|
}, [checkAuctionStatus, fetchUpdatedProject]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
const timerId = setInterval(() => {
|
||||||
|
fetchData();
|
||||||
|
}, WAIT_DURATION);
|
||||||
|
|
||||||
|
return () => clearInterval(timerId);
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
const renderAuctionStatus = useCallback(
|
||||||
|
() => (
|
||||||
|
<Tag
|
||||||
|
leftIcon={getIconByAuctionStatus(auctionStatus)}
|
||||||
|
size="xs"
|
||||||
|
type={auctionStatus === 'completed' ? 'positive' : 'emphasized'}
|
||||||
|
>
|
||||||
|
{auctionStatus.toUpperCase()}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
[auctionStatus],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpenDialog = () => setOpenDialog(true);
|
||||||
|
const handleCloseDialog = () => setOpenDialog(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="p-3 gap-2 rounded-xl border border-gray-200 transition-colors hover:bg-base-bg-alternate flex flex-col mt-8">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Heading className="text-lg leading-6 font-medium">
|
||||||
|
Auction details
|
||||||
|
</Heading>
|
||||||
|
<Button onClick={handleOpenDialog} variant="tertiary" size="sm">
|
||||||
|
View details
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center mt-2">
|
||||||
|
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
||||||
|
Auction Id
|
||||||
|
</span>
|
||||||
|
<span className="text-elements-mid-em text-sm text-right">
|
||||||
|
{project.auctionId}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center mt-1">
|
||||||
|
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
||||||
|
Auction Status
|
||||||
|
</span>
|
||||||
|
<div className="ml-2">{renderAuctionStatus()}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{auctionStatus === 'completed' && (
|
||||||
|
<>
|
||||||
|
{deployers?.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
||||||
|
Deployer LRNs
|
||||||
|
</span>
|
||||||
|
{deployers.map((deployer, index) => (
|
||||||
|
<p key={index} className="text-elements-mid-em text-sm">
|
||||||
|
{'\u2022'} {deployer.deployerLrn}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center mt-1">
|
||||||
|
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
||||||
|
Deployer Funds Status
|
||||||
|
</span>
|
||||||
|
<div className="ml-2">
|
||||||
|
<Tag
|
||||||
|
size="xs"
|
||||||
|
type={fundsStatus ? 'positive' : 'emphasized'}
|
||||||
|
>
|
||||||
|
{fundsStatus ? 'RELEASED' : 'WAITING'}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-3">
|
||||||
|
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
||||||
|
No winning deployers
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={openDialog}
|
||||||
|
onClose={handleCloseDialog}
|
||||||
|
fullWidth
|
||||||
|
maxWidth="md"
|
||||||
|
>
|
||||||
|
<DialogTitle>Auction Details</DialogTitle>
|
||||||
|
<DialogContent style={DIALOG_STYLE}>
|
||||||
|
{auctionDetails && (
|
||||||
|
<pre>{JSON.stringify(auctionDetails, null, 2)}</pre>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleCloseDialog}>Close</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,13 +1,13 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { AddProjectMemberInput, Permission } from 'gql-client';
|
|
||||||
|
|
||||||
import { Typography } from '@snowballtools/material-tailwind-react-fork';
|
import { Typography } from '@snowballtools/material-tailwind-react-fork';
|
||||||
|
|
||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import { Modal } from 'components/shared/Modal';
|
import { Modal } from 'components/shared/Modal';
|
||||||
import { Input } from 'components/shared/Input';
|
import { Input } from 'components/shared/Input';
|
||||||
import { Checkbox } from 'components/shared/Checkbox';
|
import { Select, SelectOption } from 'components/shared/Select';
|
||||||
|
import { AddProjectMemberInput, Permission } from 'gql-client';
|
||||||
|
|
||||||
interface AddMemberDialogProp {
|
interface AddMemberDialogProp {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -17,18 +17,30 @@ interface AddMemberDialogProp {
|
|||||||
|
|
||||||
interface formData {
|
interface formData {
|
||||||
emailAddress: string;
|
emailAddress: string;
|
||||||
permissions: {
|
canEdit: boolean;
|
||||||
view: boolean;
|
|
||||||
edit: boolean;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const permissionViewOptions: SelectOption = {
|
||||||
|
value: Permission.View,
|
||||||
|
label: Permission.View,
|
||||||
|
};
|
||||||
|
const permissionEditOptions: SelectOption = {
|
||||||
|
value: Permission.Edit,
|
||||||
|
label: Permission.Edit,
|
||||||
|
};
|
||||||
|
const permissionsDropdownOptions: SelectOption[] = [
|
||||||
|
permissionViewOptions,
|
||||||
|
permissionEditOptions,
|
||||||
|
];
|
||||||
|
|
||||||
const AddMemberDialog = ({
|
const AddMemberDialog = ({
|
||||||
open,
|
open,
|
||||||
handleOpen,
|
handleOpen,
|
||||||
handleAddMember,
|
handleAddMember,
|
||||||
}: AddMemberDialogProp) => {
|
}: AddMemberDialogProp) => {
|
||||||
const {
|
const {
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
reset,
|
reset,
|
||||||
@ -36,10 +48,7 @@ const AddMemberDialog = ({
|
|||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
emailAddress: '',
|
emailAddress: '',
|
||||||
permissions: {
|
canEdit: false,
|
||||||
view: true,
|
|
||||||
edit: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -47,11 +56,7 @@ const AddMemberDialog = ({
|
|||||||
reset();
|
reset();
|
||||||
handleOpen();
|
handleOpen();
|
||||||
|
|
||||||
const permissions = Object.entries(data.permissions)
|
const permissions = [data.canEdit ? Permission.Edit : Permission.View];
|
||||||
.filter(([, value]) => value)
|
|
||||||
.map(
|
|
||||||
([key]) => key.charAt(0).toUpperCase() + key.slice(1),
|
|
||||||
) as Permission[];
|
|
||||||
|
|
||||||
await handleAddMember({ email: data.emailAddress, permissions });
|
await handleAddMember({ email: data.emailAddress, permissions });
|
||||||
}, []);
|
}, []);
|
||||||
@ -72,19 +77,19 @@ const AddMemberDialog = ({
|
|||||||
required: 'email field cannot be empty',
|
required: 'email field cannot be empty',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Typography variant="small">Permissions</Typography>
|
<Select
|
||||||
<Typography variant="small">
|
label="Permissions"
|
||||||
You can change this later if required.
|
description="You can change this later if required."
|
||||||
</Typography>
|
options={permissionsDropdownOptions}
|
||||||
<Checkbox
|
value={
|
||||||
label={Permission.View}
|
watch('canEdit') ? permissionEditOptions : permissionViewOptions
|
||||||
{...register(`permissions.view`)}
|
}
|
||||||
color="blue"
|
onChange={(value) =>
|
||||||
/>
|
setValue(
|
||||||
<Checkbox
|
'canEdit',
|
||||||
label={Permission.Edit}
|
(value as SelectOption)!.value === Permission.Edit,
|
||||||
{...register(`permissions.edit`)}
|
)
|
||||||
color="blue"
|
}
|
||||||
/>
|
/>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
|
@ -61,7 +61,9 @@ const DeleteProjectDialog = ({
|
|||||||
<Input
|
<Input
|
||||||
label={
|
label={
|
||||||
"Deleting your project is irreversible. Enter your project's name " +
|
"Deleting your project is irreversible. Enter your project's name " +
|
||||||
|
'"' +
|
||||||
project.name +
|
project.name +
|
||||||
|
'"' +
|
||||||
' below to confirm you want to permanently delete it:'
|
' below to confirm you want to permanently delete it:'
|
||||||
}
|
}
|
||||||
id="input"
|
id="input"
|
||||||
|
@ -2,7 +2,6 @@ import { useState } from 'react';
|
|||||||
import { Domain, DomainStatus, Project } from 'gql-client';
|
import { Domain, DomainStatus, Project } from 'gql-client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Chip,
|
|
||||||
Typography,
|
Typography,
|
||||||
Menu,
|
Menu,
|
||||||
MenuHandler,
|
MenuHandler,
|
||||||
@ -15,6 +14,15 @@ import EditDomainDialog from './EditDomainDialog';
|
|||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog';
|
import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog';
|
||||||
import { useToast } from 'components/shared/Toast';
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
import { Tag } from 'components/shared/Tag';
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
CrossIcon,
|
||||||
|
GearIcon,
|
||||||
|
LoadingIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { Heading } from 'components/shared/Heading';
|
||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
|
||||||
enum RefreshStatus {
|
enum RefreshStatus {
|
||||||
IDLE,
|
IDLE,
|
||||||
@ -79,22 +87,29 @@ const DomainCard = ({
|
|||||||
<>
|
<>
|
||||||
<div className="flex justify-between py-3">
|
<div className="flex justify-between py-3">
|
||||||
<div className="flex justify-start gap-1">
|
<div className="flex justify-start gap-1">
|
||||||
<Typography variant="h6">
|
<Heading as="h6" className="flex-col">
|
||||||
<i>^</i> {domain.name}
|
{domain.name}{' '}
|
||||||
</Typography>
|
<Tag
|
||||||
<Chip
|
type={
|
||||||
className="w-fit capitalize"
|
domain.status === DomainStatus.Live ? 'positive' : 'negative'
|
||||||
value={domain.status}
|
}
|
||||||
color={domain.status === DomainStatus.Live ? 'green' : 'orange'}
|
leftIcon={
|
||||||
variant="ghost"
|
domain.status === DomainStatus.Live ? (
|
||||||
icon={<i>^</i>}
|
<CheckIcon />
|
||||||
/>
|
) : (
|
||||||
|
<CrossIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{domain.status}
|
||||||
|
</Tag>
|
||||||
|
</Heading>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-start gap-1">
|
<div className="flex justify-start gap-1">
|
||||||
<i
|
<i
|
||||||
id="refresh"
|
id="refresh"
|
||||||
className="cursor-pointer w-8 h-8"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
SetRefreshStatus(RefreshStatus.CHECKING);
|
SetRefreshStatus(RefreshStatus.CHECKING);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -102,11 +117,17 @@ const DomainCard = ({
|
|||||||
}, CHECK_FAIL_TIMEOUT);
|
}, CHECK_FAIL_TIMEOUT);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{refreshStatus === RefreshStatus.CHECKING ? 'L' : 'R'}
|
{refreshStatus === RefreshStatus.CHECKING ? (
|
||||||
|
<LoadingIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
'L'
|
||||||
|
)}
|
||||||
</i>
|
</i>
|
||||||
<Menu placement="bottom-end">
|
<Menu placement="bottom-end">
|
||||||
<MenuHandler>
|
<MenuHandler>
|
||||||
<button className="border-2 rounded-full w-8 h-8">...</button>
|
<Button iconOnly>
|
||||||
|
<GearIcon />
|
||||||
|
</Button>
|
||||||
</MenuHandler>
|
</MenuHandler>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -143,13 +164,13 @@ const DomainCard = ({
|
|||||||
{domain.status === DomainStatus.Pending && (
|
{domain.status === DomainStatus.Pending && (
|
||||||
<Card className="bg-slate-100 p-4 text-sm">
|
<Card className="bg-slate-100 p-4 text-sm">
|
||||||
{refreshStatus === RefreshStatus.IDLE ? (
|
{refreshStatus === RefreshStatus.IDLE ? (
|
||||||
<Typography variant="small">
|
<Heading>
|
||||||
^ Add these records to your domain and refresh to check
|
^ Add these records to your domain and refresh to check
|
||||||
</Typography>
|
</Heading>
|
||||||
) : refreshStatus === RefreshStatus.CHECKING ? (
|
) : refreshStatus === RefreshStatus.CHECKING ? (
|
||||||
<Typography variant="small" className="text-blue-500">
|
<Heading className="text-blue-500">
|
||||||
^ Checking records for {domain.name}
|
^ Checking records for {domain.name}
|
||||||
</Typography>
|
</Heading>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex gap-2 text-red-500 mb-2">
|
<div className="flex gap-2 text-red-500 mb-2">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Controller, useForm, SubmitHandler } from 'react-hook-form';
|
import { Controller, useForm, SubmitHandler } from 'react-hook-form';
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import { Domain } from 'gql-client';
|
import { Domain } from 'gql-client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -9,10 +8,11 @@ import {
|
|||||||
Option,
|
Option,
|
||||||
} from '@snowballtools/material-tailwind-react-fork';
|
} from '@snowballtools/material-tailwind-react-fork';
|
||||||
|
|
||||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
import { Modal } from 'components/shared/Modal';
|
import { Modal } from 'components/shared/Modal';
|
||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import { Input } from 'components/shared/Input';
|
import { Input } from 'components/shared/Input';
|
||||||
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
|
||||||
const DEFAULT_REDIRECT_OPTIONS = ['none'];
|
const DEFAULT_REDIRECT_OPTIONS = ['none'];
|
||||||
|
|
||||||
@ -40,6 +40,7 @@ const EditDomainDialog = ({
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
}: EditDomainDialogProp) => {
|
}: EditDomainDialogProp) => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
|
const { toast, dismiss } = useToast();
|
||||||
|
|
||||||
const getRedirectUrl = (domain: Domain) => {
|
const getRedirectUrl = (domain: Domain) => {
|
||||||
const redirectDomain = domain.redirectTo;
|
const redirectDomain = domain.redirectTo;
|
||||||
@ -99,10 +100,20 @@ const EditDomainDialog = ({
|
|||||||
|
|
||||||
if (updateDomain) {
|
if (updateDomain) {
|
||||||
await onUpdate();
|
await onUpdate();
|
||||||
toast.success(`Domain ${domain.name} has been updated`);
|
toast({
|
||||||
|
id: 'domain_id_updated',
|
||||||
|
title: `Domain ${domain.name} has been updated`,
|
||||||
|
variant: 'success',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
reset();
|
reset();
|
||||||
toast.error(`Error updating domain ${domain.name}`);
|
toast({
|
||||||
|
id: 'domain_id_error_update',
|
||||||
|
title: `Error updating domain ${domain.name}`,
|
||||||
|
variant: 'error',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOpen();
|
handleOpen();
|
||||||
|
@ -3,6 +3,8 @@ import { useState } from 'react';
|
|||||||
import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog';
|
import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog';
|
||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import { useToast } from 'components/shared/Toast';
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
import { Input } from 'components/shared/Input';
|
||||||
|
import { CopyIcon, TrashIcon } from 'components/shared/CustomIcon';
|
||||||
|
|
||||||
interface WebhookCardProps {
|
interface WebhookCardProps {
|
||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
@ -14,11 +16,12 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
|||||||
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between w-full mb-3">
|
<div className="flex justify-between w-full mb-3 gap-3">
|
||||||
{webhookUrl}
|
<Input value={webhookUrl} disabled />
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
iconOnly
|
||||||
|
size="md"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(webhookUrl);
|
navigator.clipboard.writeText(webhookUrl);
|
||||||
toast({
|
toast({
|
||||||
@ -29,16 +32,17 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Copy
|
<CopyIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
iconOnly
|
||||||
|
size="md"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDeleteDialogOpen(true);
|
setDeleteDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
X
|
<TrashIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<DeleteWebhookDialog
|
<DeleteWebhookDialog
|
||||||
|
@ -12,7 +12,7 @@ export const CollaboratorsIcon = (props: CustomIconProps) => {
|
|||||||
<path
|
<path
|
||||||
d="M12.5002 2.08301C14.3411 2.08301 15.8335 3.57539 15.8335 5.41634C15.8335 7.25729 14.3411 8.74968 12.5002 8.74968M14.5835 11.1659C17.2451 12.0374 19.1668 14.498 19.1668 17.083H16.6668M7.50016 8.74968C5.65921 8.74968 4.16683 7.25729 4.16683 5.41634C4.16683 3.57539 5.65921 2.08301 7.50016 2.08301C9.34111 2.08301 10.8335 3.57539 10.8335 5.41634C10.8335 7.25729 9.34111 8.74968 7.50016 8.74968ZM0.833496 17.083C0.833496 13.8613 3.81826 10.833 7.50016 10.833C11.1821 10.833 14.1668 13.8613 14.1668 17.083H0.833496Z"
|
d="M12.5002 2.08301C14.3411 2.08301 15.8335 3.57539 15.8335 5.41634C15.8335 7.25729 14.3411 8.74968 12.5002 8.74968M14.5835 11.1659C17.2451 12.0374 19.1668 14.498 19.1668 17.083H16.6668M7.50016 8.74968C5.65921 8.74968 4.16683 7.25729 4.16683 5.41634C4.16683 3.57539 5.65921 2.08301 7.50016 2.08301C9.34111 2.08301 10.8335 3.57539 10.8335 5.41634C10.8335 7.25729 9.34111 8.74968 7.50016 8.74968ZM0.833496 17.083C0.833496 13.8613 3.81826 10.833 7.50016 10.833C11.1821 10.833 14.1668 13.8613 14.1668 17.083H0.833496Z"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-linecap="square"
|
strokeLinecap="square"
|
||||||
/>
|
/>
|
||||||
</CustomIcon>
|
</CustomIcon>
|
||||||
);
|
);
|
||||||
|
@ -12,8 +12,8 @@ export const CopyUnfilledIcon = (props: CustomIconProps) => {
|
|||||||
<path
|
<path
|
||||||
d="M6 5.625V5.025C6 4.18492 6 3.76488 6.16349 3.44401C6.3073 3.16177 6.53677 2.9323 6.81901 2.78849C7.13988 2.625 7.55992 2.625 8.4 2.625H12.975C13.8151 2.625 14.2351 2.625 14.556 2.78849C14.8382 2.9323 15.0677 3.16177 15.2115 3.44401C15.375 3.76488 15.375 4.18492 15.375 5.025V9.975C15.375 10.8151 15.375 11.2351 15.2115 11.556C15.0677 11.8382 14.8382 12.0677 14.556 12.2115C14.2351 12.375 13.8151 12.375 12.975 12.375H12.375M12.375 8.025V12.975C12.375 13.8151 12.375 14.2351 12.2115 14.556C12.0677 14.8382 11.8382 15.0677 11.556 15.2115C11.2351 15.375 10.8151 15.375 9.975 15.375H5.025C4.18492 15.375 3.76488 15.375 3.44401 15.2115C3.16177 15.0677 2.9323 14.8382 2.78849 14.556C2.625 14.2351 2.625 13.8151 2.625 12.975V8.025C2.625 7.18492 2.625 6.76488 2.78849 6.44401C2.9323 6.16177 3.16177 5.9323 3.44401 5.78849C3.76488 5.625 4.18492 5.625 5.025 5.625H9.975C10.8151 5.625 11.2351 5.625 11.556 5.78849C11.8382 5.9323 12.0677 6.16177 12.2115 6.44401C12.375 6.76488 12.375 7.18492 12.375 8.025Z"
|
d="M6 5.625V5.025C6 4.18492 6 3.76488 6.16349 3.44401C6.3073 3.16177 6.53677 2.9323 6.81901 2.78849C7.13988 2.625 7.55992 2.625 8.4 2.625H12.975C13.8151 2.625 14.2351 2.625 14.556 2.78849C14.8382 2.9323 15.0677 3.16177 15.2115 3.44401C15.375 3.76488 15.375 4.18492 15.375 5.025V9.975C15.375 10.8151 15.375 11.2351 15.2115 11.556C15.0677 11.8382 14.8382 12.0677 14.556 12.2115C14.2351 12.375 13.8151 12.375 12.975 12.375H12.375M12.375 8.025V12.975C12.375 13.8151 12.375 14.2351 12.2115 14.556C12.0677 14.8382 11.8382 15.0677 11.556 15.2115C11.2351 15.375 10.8151 15.375 9.975 15.375H5.025C4.18492 15.375 3.76488 15.375 3.44401 15.2115C3.16177 15.0677 2.9323 14.8382 2.78849 14.556C2.625 14.2351 2.625 13.8151 2.625 12.975V8.025C2.625 7.18492 2.625 6.76488 2.78849 6.44401C2.9323 6.16177 3.16177 5.9323 3.44401 5.78849C3.76488 5.625 4.18492 5.625 5.025 5.625H9.975C10.8151 5.625 11.2351 5.625 11.556 5.78849C11.8382 5.9323 12.0677 6.16177 12.2115 6.44401C12.375 6.76488 12.375 7.18492 12.375 8.025Z"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
strokeLinecap="round"
|
||||||
stroke-linejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
</CustomIcon>
|
</CustomIcon>
|
||||||
);
|
);
|
||||||
|
@ -10,8 +10,8 @@ export const TrashIcon: React.FC<CustomIconProps> = (props) => {
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fillRule="evenodd"
|
||||||
clip-rule="evenodd"
|
clipRule="evenodd"
|
||||||
d="M5.05612 3.33398C5.52062 2.16265 6.66341 1.33398 8.00079 1.33398C9.33816 1.33398 10.481 2.16265 10.9455 3.33398H14.1668C14.443 3.33398 14.6668 3.55784 14.6668 3.83398C14.6668 4.11013 14.443 4.33398 14.1668 4.33398H13.3023L12.7463 12.952C12.684 13.9167 11.8834 14.6673 10.9167 14.6673H5.08358C4.11688 14.6673 3.31629 13.9167 3.25405 12.952L2.69805 4.33398H1.8335C1.55735 4.33398 1.3335 4.11013 1.3335 3.83398C1.3335 3.55784 1.55735 3.33398 1.8335 3.33398H5.05612ZM6.17457 3.33398C6.55973 2.73248 7.23408 2.33398 8.00079 2.33398C8.76749 2.33398 9.44184 2.73248 9.827 3.33398H6.17457ZM7.00016 7.16732C7.00016 6.89118 6.77631 6.66732 6.50016 6.66732C6.22402 6.66732 6.00016 6.89118 6.00016 7.16732V10.834C6.00016 11.1101 6.22402 11.334 6.50016 11.334C6.77631 11.334 7.00016 11.1101 7.00016 10.834V7.16732ZM9.50016 6.66732C9.77631 6.66732 10.0002 6.89118 10.0002 7.16732V10.834C10.0002 11.1101 9.77631 11.334 9.50016 11.334C9.22402 11.334 9.00016 11.1101 9.00016 10.834V7.16732C9.00016 6.89118 9.22402 6.66732 9.50016 6.66732Z"
|
d="M5.05612 3.33398C5.52062 2.16265 6.66341 1.33398 8.00079 1.33398C9.33816 1.33398 10.481 2.16265 10.9455 3.33398H14.1668C14.443 3.33398 14.6668 3.55784 14.6668 3.83398C14.6668 4.11013 14.443 4.33398 14.1668 4.33398H13.3023L12.7463 12.952C12.684 13.9167 11.8834 14.6673 10.9167 14.6673H5.08358C4.11688 14.6673 3.31629 13.9167 3.25405 12.952L2.69805 4.33398H1.8335C1.55735 4.33398 1.3335 4.11013 1.3335 3.83398C1.3335 3.55784 1.55735 3.33398 1.8335 3.33398H5.05612ZM6.17457 3.33398C6.55973 2.73248 7.23408 2.33398 8.00079 2.33398C8.76749 2.33398 9.44184 2.73248 9.827 3.33398H6.17457ZM7.00016 7.16732C7.00016 6.89118 6.77631 6.66732 6.50016 6.66732C6.22402 6.66732 6.00016 6.89118 6.00016 7.16732V10.834C6.00016 11.1101 6.22402 11.334 6.50016 11.334C6.77631 11.334 7.00016 11.1101 7.00016 10.834V7.16732ZM9.50016 6.66732C9.77631 6.66732 10.0002 6.89118 10.0002 7.16732V10.834C10.0002 11.1101 9.77631 11.334 9.50016 11.334C9.22402 11.334 9.00016 11.1101 9.00016 10.834V7.16732C9.00016 6.89118 9.22402 6.66732 9.50016 6.66732Z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
ComponentPropsWithoutRef,
|
ComponentPropsWithoutRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { FieldValues, UseFormRegister } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { WarningIcon } from 'components/shared/CustomIcon';
|
import { WarningIcon } from 'components/shared/CustomIcon';
|
||||||
import { cloneIcon } from 'utils/cloneIcon';
|
import { cloneIcon } from 'utils/cloneIcon';
|
||||||
@ -12,7 +11,7 @@ import { cn } from 'utils/classnames';
|
|||||||
|
|
||||||
import { InputTheme, inputTheme } from './Input.theme';
|
import { InputTheme, inputTheme } from './Input.theme';
|
||||||
|
|
||||||
export interface InputProps<T extends FieldValues = FieldValues>
|
export interface InputProps
|
||||||
extends InputTheme,
|
extends InputTheme,
|
||||||
Omit<ComponentPropsWithoutRef<'input'>, 'size'> {
|
Omit<ComponentPropsWithoutRef<'input'>, 'size'> {
|
||||||
label?: string;
|
label?: string;
|
||||||
@ -20,9 +19,6 @@ export interface InputProps<T extends FieldValues = FieldValues>
|
|||||||
leftIcon?: ReactNode;
|
leftIcon?: ReactNode;
|
||||||
rightIcon?: ReactNode;
|
rightIcon?: ReactNode;
|
||||||
helperText?: string;
|
helperText?: string;
|
||||||
|
|
||||||
// react-hook-form optional register
|
|
||||||
register?: ReturnType<UseFormRegister<T>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||||
@ -34,7 +30,6 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
leftIcon,
|
leftIcon,
|
||||||
rightIcon,
|
rightIcon,
|
||||||
helperText,
|
helperText,
|
||||||
register,
|
|
||||||
size,
|
size,
|
||||||
state,
|
state,
|
||||||
appearance,
|
appearance,
|
||||||
@ -107,12 +102,11 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
<div className={containerCls({ class: className })}>
|
<div className={containerCls({ class: className })}>
|
||||||
{leftIcon && renderLeftIcon}
|
{leftIcon && renderLeftIcon}
|
||||||
<input
|
<input
|
||||||
{...(register ? register : {})}
|
|
||||||
className={cn(inputCls(), {
|
className={cn(inputCls(), {
|
||||||
'pl-10': leftIcon,
|
'pl-10': leftIcon,
|
||||||
})}
|
})}
|
||||||
{...props}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
{rightIcon && renderRightIcon}
|
{rightIcon && renderRightIcon}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { NavLink, useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Organization, User } from 'gql-client';
|
import { User } from 'gql-client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import { useDisconnect } from 'wagmi';
|
||||||
|
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
import {
|
import {
|
||||||
@ -18,8 +19,7 @@ import { getInitials } from 'utils/geInitials';
|
|||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import { cn } from 'utils/classnames';
|
import { cn } from 'utils/classnames';
|
||||||
import { useMediaQuery } from 'usehooks-ts';
|
import { useMediaQuery } from 'usehooks-ts';
|
||||||
import { SIDEBAR_MENU } from './constants';
|
import { BASE_URL } from 'utils/constants';
|
||||||
import { UserSelect } from 'components/shared/UserSelect';
|
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
mobileOpen?: boolean;
|
mobileOpen?: boolean;
|
||||||
@ -32,6 +32,7 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
|||||||
const isDesktop = useMediaQuery('(min-width: 960px)');
|
const isDesktop = useMediaQuery('(min-width: 960px)');
|
||||||
|
|
||||||
const [user, setUser] = useState<User>();
|
const [user, setUser] = useState<User>();
|
||||||
|
const { disconnect } = useDisconnect();
|
||||||
|
|
||||||
const fetchUser = useCallback(async () => {
|
const fetchUser = useCallback(async () => {
|
||||||
const { user } = await client.getUser();
|
const { user } = await client.getUser();
|
||||||
@ -42,54 +43,15 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
|||||||
fetchUser();
|
fetchUser();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [selectedOrgSlug, setSelectedOrgSlug] = useState(orgSlug);
|
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
|
||||||
|
|
||||||
const fetchUserOrganizations = useCallback(async () => {
|
|
||||||
const { organizations } = await client.getOrganizations();
|
|
||||||
setOrganizations(organizations);
|
|
||||||
}, [orgSlug]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchUserOrganizations();
|
|
||||||
setSelectedOrgSlug(orgSlug);
|
|
||||||
}, [orgSlug]);
|
|
||||||
|
|
||||||
const formattedSelected = useMemo(() => {
|
|
||||||
const selected = organizations.find((org) => org.slug === selectedOrgSlug);
|
|
||||||
return {
|
|
||||||
value: selected?.slug ?? '',
|
|
||||||
label: selected?.name ?? '',
|
|
||||||
imgSrc: '/logo.svg',
|
|
||||||
};
|
|
||||||
}, [organizations, selectedOrgSlug, orgSlug]);
|
|
||||||
|
|
||||||
const formattedSelectOptions = useMemo(() => {
|
|
||||||
return organizations.map((org) => ({
|
|
||||||
value: org.slug,
|
|
||||||
label: org.name,
|
|
||||||
imgSrc: '/logo.svg',
|
|
||||||
}));
|
|
||||||
}, [organizations, selectedOrgSlug, orgSlug]);
|
|
||||||
|
|
||||||
const renderMenu = useMemo(() => {
|
|
||||||
return SIDEBAR_MENU(orgSlug).map(({ title, icon, url }, index) => (
|
|
||||||
<NavLink to={url} key={index}>
|
|
||||||
<Tabs.Trigger icon={icon} value={title}>
|
|
||||||
{title}
|
|
||||||
</Tabs.Trigger>
|
|
||||||
</NavLink>
|
|
||||||
));
|
|
||||||
}, [orgSlug]);
|
|
||||||
|
|
||||||
const handleLogOut = useCallback(async () => {
|
const handleLogOut = useCallback(async () => {
|
||||||
await fetch(`${import.meta.env.VITE_SERVER_URL}/auth/logout`, {
|
await fetch(`${BASE_URL}/auth/logout`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
disconnect();
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
}, [navigate]);
|
}, [disconnect, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.nav
|
<motion.nav
|
||||||
@ -113,16 +75,8 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
|||||||
<div className="hidden lg:flex">
|
<div className="hidden lg:flex">
|
||||||
<Logo orgSlug={orgSlug} />
|
<Logo orgSlug={orgSlug} />
|
||||||
</div>
|
</div>
|
||||||
{/* Switch organization */}
|
{/* This element ensures the space between logo and navigation */}
|
||||||
<div className="flex flex-1 flex-col gap-4">
|
<div className="flex-1"></div>
|
||||||
<UserSelect
|
|
||||||
value={formattedSelected}
|
|
||||||
options={formattedSelectOptions}
|
|
||||||
/>
|
|
||||||
<Tabs defaultValue="Projects" orientation="vertical">
|
|
||||||
<Tabs.List>{renderMenu}</Tabs.List>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
{/* Bottom navigation */}
|
{/* Bottom navigation */}
|
||||||
<div className="flex flex-col gap-5 justify-end">
|
<div className="flex flex-col gap-5 justify-end">
|
||||||
<Tabs defaultValue="Projects" orientation="vertical">
|
<Tabs defaultValue="Projects" orientation="vertical">
|
||||||
|
@ -103,10 +103,12 @@ export const UserSelect = ({ options, value }: UserSelectProps) => {
|
|||||||
<div className={theme.container()}>
|
<div className={theme.container()}>
|
||||||
{/* Input */}
|
{/* Input */}
|
||||||
<div
|
<div
|
||||||
{...getToggleButtonProps({
|
{...getToggleButtonProps(
|
||||||
ref: inputWrapperRef,
|
{
|
||||||
suppressRefError: true,
|
ref: inputWrapperRef,
|
||||||
})}
|
},
|
||||||
|
{ suppressRefError: true },
|
||||||
|
)}
|
||||||
onClick={() => !dropdownOpen && openMenu()}
|
onClick={() => !dropdownOpen && openMenu()}
|
||||||
className="cursor-pointer relative py-2 pl-2 pr-4 flex min-w-[200px] w-full items-center justify-between rounded-xl bg-surface-card shadow-sm"
|
className="cursor-pointer relative py-2 pl-2 pr-4 flex min-w-[200px] w-full items-center justify-between rounded-xl bg-surface-card shadow-sm"
|
||||||
>
|
>
|
||||||
|
@ -0,0 +1,117 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { Input } from '../Input';
|
||||||
|
|
||||||
|
export interface VerifyCodeInputProps {
|
||||||
|
code: string;
|
||||||
|
setCode: (code: string) => void;
|
||||||
|
submitCode: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VerifyCodeInput = ({
|
||||||
|
code,
|
||||||
|
setCode,
|
||||||
|
submitCode,
|
||||||
|
loading,
|
||||||
|
}: VerifyCodeInputProps) => {
|
||||||
|
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||||
|
|
||||||
|
const handlePaste = (
|
||||||
|
e: React.ClipboardEvent<HTMLInputElement>,
|
||||||
|
i: number,
|
||||||
|
) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const pasteData = e.clipboardData.getData('text').replace(/\D/g, ''); // Only digits
|
||||||
|
if (pasteData.length > 0) {
|
||||||
|
let newCodeArray = code.split('');
|
||||||
|
for (let j = 0; j < pasteData.length && i + j < 6; j++) {
|
||||||
|
newCodeArray[i + j] = pasteData[j];
|
||||||
|
}
|
||||||
|
const newCode = newCodeArray.join('');
|
||||||
|
setCode(newCode);
|
||||||
|
const nextIndex = Math.min(i + pasteData.length, 5);
|
||||||
|
const nextInput = inputRefs.current[nextIndex];
|
||||||
|
if (nextInput) nextInput.focus();
|
||||||
|
if (!newCode.includes(' ')) {
|
||||||
|
submitCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (
|
||||||
|
e: React.KeyboardEvent<HTMLInputElement>,
|
||||||
|
i: number,
|
||||||
|
) => {
|
||||||
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
||||||
|
|
||||||
|
if (e.key === 'Backspace') {
|
||||||
|
e.preventDefault();
|
||||||
|
const isEmpty = code[i] === ' ';
|
||||||
|
const newCode = !isEmpty
|
||||||
|
? code.slice(0, i) + ' ' + code.slice(i + 1, 6)
|
||||||
|
: code.slice(0, i - 1) + ' ' + code.slice(i, 6);
|
||||||
|
|
||||||
|
setCode(newCode.slice(0, 6));
|
||||||
|
|
||||||
|
if (i === 0 || !isEmpty) return;
|
||||||
|
const prev = inputRefs.current[i - 1];
|
||||||
|
if (prev) prev.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!e.key.match(/[0-9]/)) return;
|
||||||
|
|
||||||
|
e.preventDefault(); // Prevent the default event to avoid duplicate input
|
||||||
|
|
||||||
|
const newCode = code.slice(0, i) + e.key + code.slice(i + 1, 6);
|
||||||
|
setCode(newCode);
|
||||||
|
|
||||||
|
if (i === 5) {
|
||||||
|
submitCode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = inputRefs.current[i + 1];
|
||||||
|
if (next) next.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>, i: number) => {
|
||||||
|
const value = e.target.value.slice(-1);
|
||||||
|
if (!value.match(/[0-9]/)) return;
|
||||||
|
|
||||||
|
const newCode = code.slice(0, i) + value + code.slice(i + 1, 6);
|
||||||
|
setCode(newCode);
|
||||||
|
|
||||||
|
if (i < 5) {
|
||||||
|
const next = inputRefs.current[i + 1];
|
||||||
|
if (next) next.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newCode.includes(' ')) {
|
||||||
|
submitCode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRefs.current[0]) {
|
||||||
|
inputRefs.current[0].focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{code.split('').map((char, i) => (
|
||||||
|
<Input
|
||||||
|
key={i}
|
||||||
|
value={char === ' ' ? '' : char}
|
||||||
|
ref={(el) => (inputRefs.current[i] = el)}
|
||||||
|
onChange={(e) => handleChange(e, i)}
|
||||||
|
onPaste={(e) => handlePaste(e, i)}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e, i)}
|
||||||
|
disabled={!!loading}
|
||||||
|
style={{ textAlign: 'center' }} // Add this line to center text
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export { VerifyCodeInput } from './VerifyCodeInput';
|
27
packages/frontend/src/components/shared/index.ts
Normal file
27
packages/frontend/src/components/shared/index.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export { Avatar } from './Avatar';
|
||||||
|
export { Badge } from './Badge';
|
||||||
|
export { Button } from './Button';
|
||||||
|
export { Calendar } from './Calendar';
|
||||||
|
export { Checkbox } from './Checkbox';
|
||||||
|
export { DatePicker } from './DatePicker';
|
||||||
|
export { DotBorder } from './DotBorder';
|
||||||
|
export { Heading } from './Heading';
|
||||||
|
export { IconWithFrame } from './IconWithFrame';
|
||||||
|
export { InlineNotification } from './InlineNotification';
|
||||||
|
export { Input } from './Input';
|
||||||
|
export { Modal } from './Modal';
|
||||||
|
export { OverflownText } from './OverflownText';
|
||||||
|
export { Radio } from './Radio';
|
||||||
|
export { SegmentedControls } from './SegmentedControls';
|
||||||
|
export { Select } from './Select';
|
||||||
|
export { Sidebar } from './Sidebar';
|
||||||
|
export { Step } from './Steps';
|
||||||
|
export { Switch } from './Switch';
|
||||||
|
export { Table } from './Table';
|
||||||
|
export { Tabs } from './Tabs';
|
||||||
|
export { Tag } from './Tag';
|
||||||
|
export { useToast } from './Toast';
|
||||||
|
export { Tooltip } from './Tooltip';
|
||||||
|
export { UserSelect } from './UserSelect';
|
||||||
|
export { VerifyCodeInput } from './VerifyCodeInput';
|
||||||
|
export { WavyBorder } from './WavyBorder';
|
210
packages/frontend/src/context/WalletConnectContext.tsx
Normal file
210
packages/frontend/src/context/WalletConnectContext.tsx
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import SignClient from '@walletconnect/sign-client';
|
||||||
|
import { getSdkError } from '@walletconnect/utils';
|
||||||
|
import { SessionTypes } from '@walletconnect/types';
|
||||||
|
|
||||||
|
import { walletConnectModal } from '../utils/web3modal';
|
||||||
|
import {
|
||||||
|
VITE_LACONICD_CHAIN_ID,
|
||||||
|
VITE_WALLET_CONNECT_ID,
|
||||||
|
} from 'utils/constants';
|
||||||
|
|
||||||
|
interface ClientInterface {
|
||||||
|
signClient: SignClient | undefined;
|
||||||
|
session: SessionTypes.Struct | undefined;
|
||||||
|
loadingSession: boolean;
|
||||||
|
onConnect: () => Promise<void>;
|
||||||
|
onDisconnect: () => Promise<void>;
|
||||||
|
onSessionDelete: () => void;
|
||||||
|
accounts: { address: string }[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClientContext = createContext({} as ClientInterface);
|
||||||
|
|
||||||
|
export const useWalletConnectClient = () => {
|
||||||
|
return useContext(ClientContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WalletConnectClientProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: JSX.Element;
|
||||||
|
}) => {
|
||||||
|
const [signClient, setSignClient] = useState<SignClient>();
|
||||||
|
const [session, setSession] = useState<SessionTypes.Struct>();
|
||||||
|
const [loadingSession, setLoadingSession] = useState(true);
|
||||||
|
const [accounts, setAccounts] = useState<{ address: string }[]>();
|
||||||
|
|
||||||
|
const isSignClientInitializing = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const onSessionConnect = useCallback(async (session: SessionTypes.Struct) => {
|
||||||
|
setSession(session);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const subscribeToEvents = useCallback(
|
||||||
|
async (client: SignClient) => {
|
||||||
|
client.on('session_update', ({ topic, params }) => {
|
||||||
|
const { namespaces } = params;
|
||||||
|
const currentSession = client.session.get(topic);
|
||||||
|
const updatedSession = { ...currentSession, namespaces };
|
||||||
|
setSession(updatedSession);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setSession],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onConnect = async () => {
|
||||||
|
const proposalNamespace = {
|
||||||
|
cosmos: {
|
||||||
|
methods: ['cosmos_sendTokens'],
|
||||||
|
chains: [`cosmos:${VITE_LACONICD_CHAIN_ID}`],
|
||||||
|
events: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { uri, approval } = await signClient!.connect({
|
||||||
|
requiredNamespaces: proposalNamespace,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uri) {
|
||||||
|
walletConnectModal.openModal({ uri });
|
||||||
|
const session = await approval();
|
||||||
|
onSessionConnect(session);
|
||||||
|
walletConnectModal.closeModal();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDisconnect = useCallback(async () => {
|
||||||
|
if (typeof signClient === 'undefined') {
|
||||||
|
throw new Error('WalletConnect is not initialized');
|
||||||
|
}
|
||||||
|
if (typeof session === 'undefined') {
|
||||||
|
throw new Error('Session is not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
await signClient.disconnect({
|
||||||
|
topic: session.topic,
|
||||||
|
reason: getSdkError('USER_DISCONNECTED'),
|
||||||
|
});
|
||||||
|
|
||||||
|
onSessionDelete();
|
||||||
|
}, [signClient, session]);
|
||||||
|
|
||||||
|
const onSessionDelete = () => {
|
||||||
|
setAccounts(undefined);
|
||||||
|
setSession(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkPersistedState = useCallback(
|
||||||
|
async (signClient: SignClient) => {
|
||||||
|
if (typeof signClient === 'undefined') {
|
||||||
|
throw new Error('WalletConnect is not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof session !== 'undefined') return;
|
||||||
|
if (signClient.session.length) {
|
||||||
|
const lastKeyIndex = signClient.session.keys.length - 1;
|
||||||
|
const previousSsession = signClient.session.get(
|
||||||
|
signClient.session.keys[lastKeyIndex],
|
||||||
|
);
|
||||||
|
|
||||||
|
await onSessionConnect(previousSsession);
|
||||||
|
return previousSsession;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[session, onSessionConnect],
|
||||||
|
);
|
||||||
|
|
||||||
|
const createClient = useCallback(async () => {
|
||||||
|
isSignClientInitializing.current = true;
|
||||||
|
try {
|
||||||
|
const signClient = await SignClient.init({
|
||||||
|
projectId: VITE_WALLET_CONNECT_ID,
|
||||||
|
metadata: {
|
||||||
|
name: 'Deploy App',
|
||||||
|
description: '',
|
||||||
|
url: window.location.href,
|
||||||
|
icons: ['https://avatars.githubusercontent.com/u/92608123'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setSignClient(signClient);
|
||||||
|
await checkPersistedState(signClient);
|
||||||
|
await subscribeToEvents(signClient);
|
||||||
|
setLoadingSession(false);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('error in createClient', e);
|
||||||
|
}
|
||||||
|
isSignClientInitializing.current = false;
|
||||||
|
}, [setSignClient, checkPersistedState, subscribeToEvents]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!signClient && !isSignClientInitializing.current) {
|
||||||
|
createClient();
|
||||||
|
}
|
||||||
|
}, [signClient, createClient]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const populateAccounts = async () => {
|
||||||
|
if (!session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!session.namespaces['cosmos']) {
|
||||||
|
console.log('Accounts for cosmos namespace not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cosmosAddresses = session.namespaces['cosmos'].accounts;
|
||||||
|
|
||||||
|
const cosmosAccounts = cosmosAddresses.map((address) => ({
|
||||||
|
address,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const allAccounts = cosmosAccounts;
|
||||||
|
|
||||||
|
setAccounts(allAccounts);
|
||||||
|
};
|
||||||
|
|
||||||
|
populateAccounts();
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!signClient) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
signClient.on('session_delete', onSessionDelete);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
signClient.off('session_delete', onSessionDelete);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClientContext.Provider
|
||||||
|
value={{
|
||||||
|
signClient,
|
||||||
|
onConnect,
|
||||||
|
onDisconnect,
|
||||||
|
onSessionDelete,
|
||||||
|
loadingSession,
|
||||||
|
session,
|
||||||
|
accounts,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ClientContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -1,14 +1,116 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
import assert from 'assert';
|
||||||
|
import { SiweMessage, generateNonce } from 'siwe';
|
||||||
|
import { WagmiProvider } from 'wagmi';
|
||||||
|
import { mainnet } from 'wagmi/chains';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { createWeb3Modal } from '@web3modal/wagmi/react';
|
||||||
|
import { defaultWagmiConfig } from '@web3modal/wagmi/react/config';
|
||||||
|
import { createSIWEConfig } from '@web3modal/siwe';
|
||||||
|
import type {
|
||||||
|
SIWECreateMessageArgs,
|
||||||
|
SIWEVerifyMessageArgs,
|
||||||
|
} from '@web3modal/core';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
import { VITE_WALLET_CONNECT_ID, BASE_URL } from 'utils/constants';
|
||||||
|
|
||||||
if (!import.meta.env.VITE_WALLET_CONNECT_ID) {
|
if (!VITE_WALLET_CONNECT_ID) {
|
||||||
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
|
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
|
||||||
}
|
}
|
||||||
|
assert(BASE_URL, 'VITE_SERVER_URL is not set in env');
|
||||||
|
|
||||||
export default function Web3Provider({ children }: { children: ReactNode }) {
|
const queryClient = new QueryClient();
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
const metadata = {
|
||||||
|
name: 'Deploy App Auth',
|
||||||
|
description: '',
|
||||||
|
url: window.location.origin,
|
||||||
|
icons: ['https://avatars.githubusercontent.com/u/37784886'],
|
||||||
|
};
|
||||||
|
const chains = [mainnet] as const;
|
||||||
|
const config = defaultWagmiConfig({
|
||||||
|
chains,
|
||||||
|
projectId: VITE_WALLET_CONNECT_ID,
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
const siweConfig = createSIWEConfig({
|
||||||
|
createMessage: ({ nonce, address, chainId }: SIWECreateMessageArgs) =>
|
||||||
|
new SiweMessage({
|
||||||
|
version: '1',
|
||||||
|
domain: window.location.host,
|
||||||
|
uri: window.location.origin,
|
||||||
|
address,
|
||||||
|
chainId,
|
||||||
|
nonce,
|
||||||
|
// Human-readable ASCII assertion that the user will sign, and it must not contain `\n`.
|
||||||
|
statement: 'Sign in With Ethereum.',
|
||||||
|
}).prepareMessage(),
|
||||||
|
getNonce: async () => {
|
||||||
|
return generateNonce();
|
||||||
|
},
|
||||||
|
getSession: async () => {
|
||||||
|
try {
|
||||||
|
const session = (await axiosInstance.get('/auth/session')).data;
|
||||||
|
const { address, chainId } = session;
|
||||||
|
return { address, chainId };
|
||||||
|
} catch (err) {
|
||||||
|
if (window.location.pathname !== '/login') {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
throw new Error('Failed to get session!');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verifyMessage: async ({ message, signature }: SIWEVerifyMessageArgs) => {
|
||||||
|
try {
|
||||||
|
const { success } = (
|
||||||
|
await axiosInstance.post('/auth/validate', {
|
||||||
|
message,
|
||||||
|
signature,
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
return success;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
signOut: async () => {
|
||||||
|
try {
|
||||||
|
const { success } = (await axiosInstance.post('/auth/logout')).data;
|
||||||
|
return success;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSignOut: () => {
|
||||||
|
window.location.href = '/login';
|
||||||
|
},
|
||||||
|
onSignIn: () => {
|
||||||
|
window.location.href = '/';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
createWeb3Modal({
|
||||||
|
siweConfig,
|
||||||
|
wagmiConfig: config,
|
||||||
|
projectId: VITE_WALLET_CONNECT_ID,
|
||||||
|
});
|
||||||
|
export default function Web3ModalProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
<WagmiProvider config={config}>
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
</WagmiProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,25 +14,34 @@ import { GQLClientProvider } from './context/GQLClientContext';
|
|||||||
import { SERVER_GQL_PATH } from './constants';
|
import { SERVER_GQL_PATH } from './constants';
|
||||||
import { Toaster } from 'components/shared/Toast';
|
import { Toaster } from 'components/shared/Toast';
|
||||||
import { LogErrorBoundary } from 'utils/log-error';
|
import { LogErrorBoundary } from 'utils/log-error';
|
||||||
|
import { BASE_URL } from 'utils/constants';
|
||||||
|
import Web3ModalProvider from './context/Web3Provider';
|
||||||
|
import { WalletConnectClientProvider } from 'context/WalletConnectContext';
|
||||||
|
|
||||||
|
console.log(`v-0.0.9`);
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement,
|
document.getElementById('root') as HTMLElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(import.meta.env.VITE_SERVER_URL, 'VITE_SERVER_URL is not set in env');
|
assert(BASE_URL, 'VITE_SERVER_URL is not set in env');
|
||||||
const gqlEndpoint = `${import.meta.env.VITE_SERVER_URL}/${SERVER_GQL_PATH}`;
|
const gqlEndpoint = `${BASE_URL}/${SERVER_GQL_PATH}`;
|
||||||
|
|
||||||
const gqlClient = new GQLClient({ gqlEndpoint });
|
const gqlClient = new GQLClient({ gqlEndpoint });
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<LogErrorBoundary>
|
<LogErrorBoundary>
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider>
|
<WalletConnectClientProvider>
|
||||||
<GQLClientProvider client={gqlClient}>
|
<ThemeProvider>
|
||||||
<App />
|
<Web3ModalProvider>
|
||||||
<Toaster />
|
<GQLClientProvider client={gqlClient}>
|
||||||
</GQLClientProvider>
|
<App />
|
||||||
</ThemeProvider>
|
<Toaster />
|
||||||
|
</GQLClientProvider>
|
||||||
|
</Web3ModalProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</WalletConnectClientProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
</LogErrorBoundary>,
|
</LogErrorBoundary>,
|
||||||
);
|
);
|
||||||
|
@ -32,7 +32,7 @@ const ProjectSearch = () => {
|
|||||||
return (
|
return (
|
||||||
<section className="h-full flex flex-col">
|
<section className="h-full flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="sticky hidden lg:block top-0 border-b bg-base-bg border-border-separator/[0.06] z-30">
|
<div className="sticky hidden lg:block top-0 border-b bg-base-bg border-border-separator/[0.06] hover:z-30">
|
||||||
<div className="flex pr-6 pl-2 py-2 items-center">
|
<div className="flex pr-6 pl-2 py-2 items-center">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<ProjectSearchBar
|
<ProjectSearchBar
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CloudyFlow } from 'components/CloudyFlow';
|
import { CloudyFlow } from 'components/CloudyFlow';
|
||||||
import { SnowballAuth } from './auth/SnowballAuth';
|
import { Login } from './auth/Login';
|
||||||
|
|
||||||
const AuthPage = () => {
|
const AuthPage = () => {
|
||||||
return (
|
return (
|
||||||
@ -18,7 +18,7 @@ const AuthPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="pb-12 relative z-10 flex-1 flex-center">
|
<div className="pb-12 relative z-10 flex-1 flex-center">
|
||||||
<div className="max-w-[520px] w-full bg-white rounded-xl shadow">
|
<div className="max-w-[520px] w-full bg-white rounded-xl shadow">
|
||||||
<SnowballAuth />
|
<Login />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CloudyFlow>
|
</CloudyFlow>
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
import { Button } from 'components/shared/Button';
|
|
||||||
import { LoaderIcon } from 'components/shared/CustomIcon';
|
|
||||||
import { KeyIcon } from 'components/shared/CustomIcon/KeyIcon';
|
|
||||||
import { InlineNotification } from 'components/shared/InlineNotification';
|
|
||||||
import { Input } from 'components/shared/Input';
|
|
||||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { IconRight } from 'react-day-picker';
|
|
||||||
import { useSnowball } from 'utils/use-snowball';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onDone: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CreatePasskey = ({}: Props) => {
|
|
||||||
const snowball = useSnowball();
|
|
||||||
const [name, setName] = useState('');
|
|
||||||
|
|
||||||
const auth = snowball.auth.passkey;
|
|
||||||
const loading = !!auth.state.loading;
|
|
||||||
|
|
||||||
async function createPasskey() {
|
|
||||||
await auth.register(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
|
|
||||||
<div className="w-16 h-16 p-2 bg-sky-100 rounded-[800px] justify-center items-center gap-2 inline-flex">
|
|
||||||
<KeyIcon />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-loose">
|
|
||||||
Create a passkey
|
|
||||||
</div>
|
|
||||||
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Passkeys allow you to sign in securely without using passwords.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<WavyBorder className="self-stretch" variant="stroke" />
|
|
||||||
<div className="p-6 flex-col justify-center items-center gap-8 inline-flex">
|
|
||||||
<div className="self-stretch h-36 flex-col justify-center items-center gap-2 flex">
|
|
||||||
<div className="self-stretch h-[72px] flex-col justify-start items-start gap-2 flex">
|
|
||||||
<div className="self-stretch h-5 px-1 flex-col justify-start items-start gap-1 flex">
|
|
||||||
<div className="self-stretch text-sky-950 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Give it a name
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
value={name}
|
|
||||||
onInput={(e: any) => {
|
|
||||||
setName(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{auth.state.error ? (
|
|
||||||
<InlineNotification
|
|
||||||
title={auth.state.error.message}
|
|
||||||
variant="danger"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<InlineNotification
|
|
||||||
title={`Once you press the "Create passkeys" button, you'll receive a prompt to create the passkey.`}
|
|
||||||
variant="info"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
rightIcon={
|
|
||||||
loading ? <LoaderIcon className="animate-spin" /> : <IconRight />
|
|
||||||
}
|
|
||||||
className="self-stretch"
|
|
||||||
disabled={!name || loading}
|
|
||||||
onClick={createPasskey}
|
|
||||||
>
|
|
||||||
Create Passkey
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,98 +1,6 @@
|
|||||||
import { Button } from 'components/shared/Button';
|
|
||||||
import {
|
|
||||||
ArrowRightCircleFilledIcon,
|
|
||||||
GithubIcon,
|
|
||||||
LinkIcon,
|
|
||||||
LoaderIcon,
|
|
||||||
QuestionMarkRoundFilledIcon,
|
|
||||||
} from 'components/shared/CustomIcon';
|
|
||||||
import { GoogleIcon } from 'components/shared/CustomIcon/GoogleIcon';
|
|
||||||
import { DotBorder } from 'components/shared/DotBorder';
|
|
||||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { CreatePasskey } from './CreatePasskey';
|
|
||||||
import { AppleIcon } from 'components/shared/CustomIcon/AppleIcon';
|
|
||||||
import { KeyIcon } from 'components/shared/CustomIcon/KeyIcon';
|
|
||||||
import { useToast } from 'components/shared/Toast';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
|
|
||||||
import { signInWithEthereum } from 'utils/siwe';
|
|
||||||
import { useSnowball } from 'utils/use-snowball';
|
|
||||||
import { logError } from 'utils/log-error';
|
|
||||||
|
|
||||||
type Provider = 'google' | 'github' | 'apple' | 'email' | 'passkey';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onDone: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Login = ({ onDone }: Props) => {
|
|
||||||
const snowball = useSnowball();
|
|
||||||
const [error, setError] = useState<string>('');
|
|
||||||
const [provider, setProvider] = useState<Provider | false>(false);
|
|
||||||
|
|
||||||
// const loading = snowball.auth.state.loading && provider;
|
|
||||||
const loading = provider;
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
if (provider === 'email') {
|
|
||||||
return <CreatePasskey onDone={onDone} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSigninRedirect() {
|
|
||||||
let wallet: PKPEthersWallet | undefined;
|
|
||||||
const { google } = snowball.auth;
|
|
||||||
if (google.canHandleOAuthRedirectBack()) {
|
|
||||||
setProvider('google');
|
|
||||||
console.log('Handling google redirect back');
|
|
||||||
try {
|
|
||||||
await google.handleOAuthRedirectBack();
|
|
||||||
wallet = await google.getEthersWallet();
|
|
||||||
const result = await signInWithEthereum(1, 'login', wallet);
|
|
||||||
if (result.error) {
|
|
||||||
setError(result.error);
|
|
||||||
setProvider(false);
|
|
||||||
wallet = undefined;
|
|
||||||
logError(new Error(result.error));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message);
|
|
||||||
logError(err);
|
|
||||||
setProvider(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if (apple.canHandleOAuthRedirectBack()) {
|
|
||||||
// setProvider('apple');
|
|
||||||
// console.log('Handling apple redirect back');
|
|
||||||
// try {
|
|
||||||
// await apple.handleOAuthRedirectBack();
|
|
||||||
// wallet = await apple.getEthersWallet();
|
|
||||||
// const result = await signInWithEthereum(1, 'login', wallet);
|
|
||||||
// if (result.error) {
|
|
||||||
// setError(result.error);
|
|
||||||
// setProvider(false);
|
|
||||||
// wallet = undefined;
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// } catch (err: any) {
|
|
||||||
// setError(err.message);
|
|
||||||
// console.log(err.message, err.name, err.details);
|
|
||||||
// setProvider(false);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (wallet) {
|
|
||||||
window.location.pathname = '/';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleSigninRedirect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
export const Login = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
|
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
|
||||||
@ -103,160 +11,8 @@ export const Login = ({ onDone }: Props) => {
|
|||||||
<WavyBorder className="self-stretch" variant="stroke" />
|
<WavyBorder className="self-stretch" variant="stroke" />
|
||||||
|
|
||||||
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
|
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
|
||||||
<div className="self-stretch p-5 bg-slate-50 rounded-xl shadow flex-col justify-center items-center gap-6 flex">
|
|
||||||
<div className="self-stretch flex-col justify-center items-center gap-4 flex">
|
|
||||||
<KeyIcon />
|
|
||||||
<div className="self-stretch flex-col justify-center items-center gap-2 flex">
|
|
||||||
<div className="self-stretch text-center text-sky-950 text-lg font-medium font-display leading-normal">
|
|
||||||
Got a Passkey?
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Use it to sign in securely without using a password.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-stretch justify-center items-stretch xxs:items-center gap-3 flex flex-col xxs:flex-row">
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
leftIcon={<QuestionMarkRoundFilledIcon />}
|
|
||||||
variant={'tertiary'}
|
|
||||||
target="_blank"
|
|
||||||
href="https://safety.google/authentication/passkey/"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'passkey' ? (
|
|
||||||
<LoaderIcon className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<ArrowRightCircleFilledIcon height="16" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={!!loading}
|
|
||||||
onClick={async () => {
|
|
||||||
setProvider('passkey');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Sign In with Passkey
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-5 justify-center items-center gap-2 inline-flex">
|
|
||||||
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Lost your passkey?
|
|
||||||
</div>
|
|
||||||
<div className="justify-center items-center gap-1.5 flex">
|
|
||||||
<button className="text-sky-950 text-sm font-normal font-['Inter'] underline leading-tight">
|
|
||||||
Recover account
|
|
||||||
</button>
|
|
||||||
<LinkIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="self-stretch justify-start items-center gap-8 inline-flex">
|
|
||||||
<DotBorder className="flex-1" />
|
|
||||||
<div className="text-center text-slate-400 text-xs font-normal font-['JetBrains Mono'] leading-none">
|
|
||||||
OR
|
|
||||||
</div>
|
|
||||||
<DotBorder className="flex-1" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="self-stretch flex-col justify-center items-center gap-3 flex">
|
<div className="self-stretch flex-col justify-center items-center gap-3 flex">
|
||||||
<Button
|
<w3m-button />
|
||||||
leftIcon={<GoogleIcon />}
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'google' ? (
|
|
||||||
<LoaderIcon className="animate-spin" />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setProvider('google');
|
|
||||||
snowball.auth.google.startOAuthRedirect();
|
|
||||||
}}
|
|
||||||
className="flex-1 self-stretch"
|
|
||||||
variant={'tertiary'}
|
|
||||||
disabled={!!loading}
|
|
||||||
>
|
|
||||||
Continue with Google
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
leftIcon={<GithubIcon />}
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'github' ? (
|
|
||||||
<LoaderIcon className="animate-spin" />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onClick={async () => {
|
|
||||||
setProvider('github');
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
||||||
setProvider(false);
|
|
||||||
toast({
|
|
||||||
id: 'coming-soon',
|
|
||||||
title: 'Sign-in with GitHub is coming soon!',
|
|
||||||
variant: 'info',
|
|
||||||
onDismiss() {},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="flex-1 self-stretch"
|
|
||||||
variant={'tertiary'}
|
|
||||||
disabled={!!loading}
|
|
||||||
>
|
|
||||||
Continue with GitHub
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
leftIcon={<AppleIcon />}
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'apple' ? (
|
|
||||||
<LoaderIcon className="animate-spin text-white" />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onClick={async () => {
|
|
||||||
setProvider('apple');
|
|
||||||
// snowball.auth.apple.startOAuthRedirect();
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
||||||
setProvider(false);
|
|
||||||
toast({
|
|
||||||
id: 'coming-soon',
|
|
||||||
title: 'Sign-in with Apple is coming soon!',
|
|
||||||
variant: 'info',
|
|
||||||
onDismiss() {},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className={`flex-1 self-stretch border-black enabled:bg-black text-white ${
|
|
||||||
loading && loading === 'apple' ? 'disabled:bg-black' : ''
|
|
||||||
}`}
|
|
||||||
variant={'tertiary'}
|
|
||||||
disabled={!!loading}
|
|
||||||
>
|
|
||||||
Continue with Apple
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{error && (
|
|
||||||
<div className="justify-center items-center gap-2 inline-flex">
|
|
||||||
<div className="text-red-500 text-sm">Error: {error}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="h-5 justify-center items-center gap-2 inline-flex">
|
|
||||||
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Don't have an account?
|
|
||||||
</div>
|
|
||||||
<div className="justify-center items-center gap-1.5 flex">
|
|
||||||
<Link
|
|
||||||
to="/signup"
|
|
||||||
className="text-sky-950 text-sm font-normal font-['Inter'] underline leading-tight"
|
|
||||||
>
|
|
||||||
Sign up now
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,261 +0,0 @@
|
|||||||
import { Button } from 'components/shared/Button';
|
|
||||||
import {
|
|
||||||
ArrowRightCircleFilledIcon,
|
|
||||||
GithubIcon,
|
|
||||||
LoaderIcon,
|
|
||||||
} from 'components/shared/CustomIcon';
|
|
||||||
import { GoogleIcon } from 'components/shared/CustomIcon/GoogleIcon';
|
|
||||||
import { DotBorder } from 'components/shared/DotBorder';
|
|
||||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useSnowball } from 'utils/use-snowball';
|
|
||||||
import { Input } from 'components/shared/Input';
|
|
||||||
import { AppleIcon } from 'components/shared/CustomIcon/AppleIcon';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { useToast } from 'components/shared/Toast';
|
|
||||||
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
|
|
||||||
import { signInWithEthereum } from 'utils/siwe';
|
|
||||||
import { logError } from 'utils/log-error';
|
|
||||||
import {
|
|
||||||
subOrganizationIdForEmail,
|
|
||||||
turnkeySignin,
|
|
||||||
turnkeySignup,
|
|
||||||
} from 'utils/turnkey-frontend';
|
|
||||||
|
|
||||||
type Provider = 'google' | 'github' | 'apple' | 'email';
|
|
||||||
|
|
||||||
type Err = { type: 'email' | 'provider'; message: string };
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onDone: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SignUp = ({ onDone }: Props) => {
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [error, setError] = useState<Err | null>();
|
|
||||||
const [provider, setProvider] = useState<Provider | false>(false);
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
|
||||||
const snowball = useSnowball();
|
|
||||||
|
|
||||||
async function handleSignupRedirect() {
|
|
||||||
let wallet: PKPEthersWallet | undefined;
|
|
||||||
const { google } = snowball.auth;
|
|
||||||
if (google.canHandleOAuthRedirectBack()) {
|
|
||||||
setProvider('google');
|
|
||||||
try {
|
|
||||||
await google.handleOAuthRedirectBack();
|
|
||||||
wallet = await google.getEthersWallet();
|
|
||||||
const result = await signInWithEthereum(1, 'signup', wallet);
|
|
||||||
if (result.error) {
|
|
||||||
setError({ type: 'provider', message: result.error });
|
|
||||||
setProvider(false);
|
|
||||||
wallet = undefined;
|
|
||||||
logError(new Error(result.error));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setError({ type: 'provider', message: err.message });
|
|
||||||
setProvider(false);
|
|
||||||
logError(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if (apple.canHandleOAuthRedirectBack()) {
|
|
||||||
// setProvider('apple');
|
|
||||||
// try {
|
|
||||||
// await apple.handleOAuthRedirectBack();
|
|
||||||
// wallet = await apple.getEthersWallet();
|
|
||||||
// const result = await signInWithEthereum(1, 'signup', wallet);
|
|
||||||
// if (result.error) {
|
|
||||||
// setError({ type: 'provider', message: result.error });
|
|
||||||
// setProvider(false);
|
|
||||||
// wallet = undefined;
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// } catch (err: any) {
|
|
||||||
// setError({ type: 'provider', message: err.message });
|
|
||||||
// setProvider(false);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (wallet) {
|
|
||||||
onDone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function authEmail() {
|
|
||||||
setProvider('email');
|
|
||||||
try {
|
|
||||||
const orgId = await subOrganizationIdForEmail(email);
|
|
||||||
console.log('orgId', orgId);
|
|
||||||
if (orgId) {
|
|
||||||
await turnkeySignin(orgId);
|
|
||||||
window.location.href = '/dashboard';
|
|
||||||
} else {
|
|
||||||
await turnkeySignup(email);
|
|
||||||
onDone();
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setError({ type: 'email', message: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleSignupRedirect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loading = provider;
|
|
||||||
const emailValid = /.@./.test(email);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
|
|
||||||
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-tight">
|
|
||||||
Sign up to Snowball
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<WavyBorder className="self-stretch" variant="stroke" />
|
|
||||||
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
|
|
||||||
<div className="self-stretch flex-col justify-center items-center gap-3 flex">
|
|
||||||
<Button
|
|
||||||
leftIcon={loading && loading === 'google' ? null : <GoogleIcon />}
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'google' ? (
|
|
||||||
<LoaderIcon className="animate-spin" />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setProvider('google');
|
|
||||||
snowball.auth.google.startOAuthRedirect();
|
|
||||||
}}
|
|
||||||
className="flex-1 self-stretch"
|
|
||||||
variant={'tertiary'}
|
|
||||||
disabled={!!loading}
|
|
||||||
>
|
|
||||||
Continue with Google
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
leftIcon={<GithubIcon />}
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'github' ? (
|
|
||||||
<LoaderIcon className="animate-spin" />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onClick={async () => {
|
|
||||||
setProvider('github');
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
||||||
setProvider(false);
|
|
||||||
toast({
|
|
||||||
id: 'coming-soon',
|
|
||||||
title: 'Sign-in with GitHub is coming soon!',
|
|
||||||
variant: 'info',
|
|
||||||
onDismiss() {},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="flex-1 self-stretch"
|
|
||||||
variant={'tertiary'}
|
|
||||||
disabled={!!loading}
|
|
||||||
>
|
|
||||||
Continue with GitHub
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
leftIcon={<AppleIcon />}
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'apple' ? (
|
|
||||||
<LoaderIcon className="animate-spin text-white" />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
onClick={async () => {
|
|
||||||
setProvider('apple');
|
|
||||||
// snowball.auth.apple.startOAuthRedirect();
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
||||||
setProvider(false);
|
|
||||||
toast({
|
|
||||||
id: 'coming-soon',
|
|
||||||
title: 'Sign-in with Apple is coming soon!',
|
|
||||||
variant: 'info',
|
|
||||||
onDismiss() {},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className={`flex-1 self-stretch border-black enabled:bg-black text-white ${
|
|
||||||
loading && loading === 'apple' ? 'disabled:bg-black' : ''
|
|
||||||
}`}
|
|
||||||
variant={'tertiary'}
|
|
||||||
disabled={!!loading}
|
|
||||||
>
|
|
||||||
Continue with Apple
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && error.type === 'provider' && (
|
|
||||||
<div className="-mt-3 justify-center items-center inline-flex">
|
|
||||||
<div className="text-red-500 text-sm">Error: {error.message}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="self-stretch justify-start items-center gap-8 inline-flex">
|
|
||||||
<DotBorder className="flex-1" />
|
|
||||||
<div className="text-center text-slate-400 text-xs font-normal font-['JetBrains Mono'] leading-none">
|
|
||||||
OR
|
|
||||||
</div>
|
|
||||||
<DotBorder className="flex-1" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="self-stretch flex-col gap-8 flex">
|
|
||||||
<div className="flex-col justify-start items-start gap-2 inline-flex">
|
|
||||||
<div className="text-sky-950 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Email
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
disabled={!!loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
rightIcon={
|
|
||||||
loading && loading === 'email' ? (
|
|
||||||
<LoaderIcon className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<ArrowRightCircleFilledIcon height="16" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
authEmail();
|
|
||||||
}}
|
|
||||||
variant={'secondary'}
|
|
||||||
disabled={!email || !emailValid || !!loading}
|
|
||||||
>
|
|
||||||
Continue with Email
|
|
||||||
</Button>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{error && error.type === 'email' && (
|
|
||||||
<div className="justify-center items-center gap-2 inline-flex">
|
|
||||||
<div className="text-red-500 text-sm">
|
|
||||||
Error: {error.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="justify-center items-center gap-2 inline-flex">
|
|
||||||
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
|
||||||
Already an user?
|
|
||||||
</div>
|
|
||||||
<div className="justify-center items-center gap-1.5 flex">
|
|
||||||
<Link
|
|
||||||
to="/login"
|
|
||||||
className="text-sky-950 text-sm font-normal font-['Inter'] underline leading-tight"
|
|
||||||
>
|
|
||||||
Sign in now
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,52 +0,0 @@
|
|||||||
import { snowball } from 'utils/use-snowball';
|
|
||||||
import { Login } from './Login';
|
|
||||||
import { SignUp } from './SignUp';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Done } from './Done';
|
|
||||||
|
|
||||||
type Screen = 'login' | 'signup' | 'success';
|
|
||||||
|
|
||||||
const DASHBOARD_URL = '/';
|
|
||||||
|
|
||||||
export const SnowballAuth = () => {
|
|
||||||
const path = window.location.pathname;
|
|
||||||
const [screen, setScreen] = useState<Screen>(
|
|
||||||
path === '/login' ? 'login' : 'signup',
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (snowball.session) {
|
|
||||||
window.location.href = DASHBOARD_URL;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (path === '/login') {
|
|
||||||
setScreen('login');
|
|
||||||
} else if (path === '/signup') {
|
|
||||||
setScreen('signup');
|
|
||||||
}
|
|
||||||
}, [path]);
|
|
||||||
|
|
||||||
if (screen === 'signup') {
|
|
||||||
return (
|
|
||||||
<SignUp
|
|
||||||
onDone={() => {
|
|
||||||
setScreen('success');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (screen === 'login') {
|
|
||||||
return (
|
|
||||||
<Login
|
|
||||||
onDone={() => {
|
|
||||||
setScreen('success');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (screen === 'success') {
|
|
||||||
return <Done continueTo={DASHBOARD_URL} />;
|
|
||||||
}
|
|
||||||
};
|
|
@ -36,6 +36,13 @@ const deployment: Deployment = {
|
|||||||
url: 'https://deploy1.example.com',
|
url: 'https://deploy1.example.com',
|
||||||
environment: Environment.Production,
|
environment: Environment.Production,
|
||||||
isCurrent: true,
|
isCurrent: true,
|
||||||
|
deployer: {
|
||||||
|
deployerApiUrl: 'https://webapp-deployer-api.example.com',
|
||||||
|
deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu',
|
||||||
|
deployerLrn: 'lrn://example/deployers/webapp-deployer-api.example.com',
|
||||||
|
minimumPayment: '1000alnt',
|
||||||
|
baseDomain: 'pwa.example.com',
|
||||||
|
},
|
||||||
status: DeploymentStatus.Ready,
|
status: DeploymentStatus.Ready,
|
||||||
createdBy: {
|
createdBy: {
|
||||||
id: 'user1',
|
id: 'user1',
|
||||||
@ -48,6 +55,8 @@ const deployment: Deployment = {
|
|||||||
},
|
},
|
||||||
createdAt: '1677676800', // 2023-03-01T12:00:00Z
|
createdAt: '1677676800', // 2023-03-01T12:00:00Z
|
||||||
updatedAt: '1677680400', // 2023-03-01T13:00:00Z
|
updatedAt: '1677680400', // 2023-03-01T13:00:00Z
|
||||||
|
applicationDeploymentRequestId:
|
||||||
|
'bafyreiaycvq6imoppnpwdve4smj6t6ql5svt5zl3x6rimu4qwyzgjorize',
|
||||||
};
|
};
|
||||||
|
|
||||||
const domains: Domain[] = [
|
const domains: Domain[] = [
|
||||||
|
@ -1,5 +1,19 @@
|
|||||||
const Settings = () => {
|
import { Heading } from 'components/shared/Heading';
|
||||||
return <div className="p-5">Settings page</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const Settings = () => {
|
||||||
|
return (
|
||||||
|
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="grow">
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<Heading as="h2" className="text-[24px]">
|
||||||
|
Settings
|
||||||
|
</Heading>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
export default Settings;
|
export default Settings;
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import { Project } from 'gql-client';
|
|
||||||
import { Button } from 'components/shared/Button';
|
|
||||||
|
|
||||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
|
||||||
import { ProjectCard } from 'components/projects/ProjectCard';
|
import { ProjectCard } from 'components/projects/ProjectCard';
|
||||||
import { Heading } from 'components/shared/Heading';
|
import { Heading, Badge, Button } from 'components/shared';
|
||||||
import { Badge } from 'components/shared/Badge';
|
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
|
import { Project } from 'gql-client';
|
||||||
|
|
||||||
const Projects = () => {
|
const Projects = () => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
@ -28,6 +26,7 @@ const Projects = () => {
|
|||||||
return (
|
return (
|
||||||
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
|
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
|
@ -92,9 +92,13 @@ const Id = () => {
|
|||||||
Open repo
|
Open repo
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button {...buttonSize} className="h-11 transition-colors">
|
{(project.deployments.length > 0) &&
|
||||||
Go to app
|
<Link to={`https://${project.name.toLowerCase()}.${project.deployments[0].deployer.baseDomain}`}>
|
||||||
</Button>
|
<Button {...buttonSize} className="h-11 transition-colors">
|
||||||
|
Go to app
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<WavyBorder />
|
<WavyBorder />
|
||||||
|
@ -31,6 +31,11 @@ const CreateWithTemplate = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
step: 2,
|
step: 2,
|
||||||
|
route: `/${orgSlug}/projects/create/template/configure`,
|
||||||
|
label: 'Configure',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: 3,
|
||||||
route: `/${orgSlug}/projects/create/template/deploy`,
|
route: `/${orgSlug}/projects/create/template/deploy`,
|
||||||
label: 'Deploy',
|
label: 'Deploy',
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,8 @@ import NewProject from './index';
|
|||||||
import CreateWithTemplate from './Template';
|
import CreateWithTemplate from './Template';
|
||||||
import { templateRoutes } from './template/routes';
|
import { templateRoutes } from './template/routes';
|
||||||
import Id from './success/Id';
|
import Id from './success/Id';
|
||||||
import Import from './Import';
|
import Configure from 'components/projects/create/Configure';
|
||||||
|
import Deploy from 'components/projects/create/Deploy';
|
||||||
|
|
||||||
export const createProjectRoutes = [
|
export const createProjectRoutes = [
|
||||||
{
|
{
|
||||||
@ -19,7 +20,11 @@ export const createProjectRoutes = [
|
|||||||
element: <Id />,
|
element: <Id />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'import',
|
path: 'configure',
|
||||||
element: <Import />,
|
element: <Configure />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'deploy',
|
||||||
|
element: <Deploy />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -1,108 +1,123 @@
|
|||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import Lottie from 'lottie-react';
|
import Lottie from 'lottie-react';
|
||||||
|
|
||||||
import { Badge } from 'components/shared/Badge';
|
import { Badge } from 'components/shared/Badge';
|
||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import {
|
import {
|
||||||
ArrowLeftCircleFilledIcon,
|
ArrowLeftCircleFilledIcon,
|
||||||
LinkChainIcon,
|
|
||||||
QuestionMarkRoundFilledIcon,
|
QuestionMarkRoundFilledIcon,
|
||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import { Heading } from 'components/shared/Heading';
|
import { Heading } from 'components/shared/Heading';
|
||||||
|
|
||||||
import logoAnimation from 'components/../../public/lottie/logo.json';
|
import logoAnimation from 'components/../../public/lottie/logo.json';
|
||||||
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
|
import { Project } from 'gql-client';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const Id = () => {
|
const Id = () => {
|
||||||
const { id, orgSlug } = useParams();
|
const { id, orgSlug } = useParams();
|
||||||
|
const client = useGQLClient();
|
||||||
|
const [project, setProject] = useState<Project | null>(null);
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const isAuction = searchParams.get('isAuction') === 'true';
|
||||||
|
|
||||||
const handleSetupDomain = () => {
|
const handleSetupDomain = async () => {
|
||||||
//TODO: Implement this
|
if (id) {
|
||||||
|
const project = await client.getProject(id);
|
||||||
|
if (project && project.project) {
|
||||||
|
setProject(project.project);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleSetupDomain();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8 lg:gap-11 max-w-[522px] mx-auto py-6 lg:py-12">
|
<>
|
||||||
{/* Icon */}
|
{project ? (
|
||||||
<div className="flex justify-center">
|
<div className="flex flex-col gap-8 lg:gap-11 max-w-[522px] mx-auto py-6 lg:py-12">
|
||||||
<Lottie animationData={logoAnimation} loop={false} size={40} />
|
{/* Icon */}
|
||||||
</div>
|
<div className="flex justify-center">
|
||||||
|
<Lottie animationData={logoAnimation} loop={false} size={40} />
|
||||||
{/* Heading */}
|
|
||||||
<div className="flex flex-col items-center gap-1.5">
|
|
||||||
<Heading as="h3" className="font-medium text-xl">
|
|
||||||
Project deployed successfully.
|
|
||||||
</Heading>
|
|
||||||
<p className="flex flex-col items-center lg:flex-row font-sans gap-0.5 lg:gap-2 text-sm text-elements-high-em">
|
|
||||||
Your project has been deployed at{' '}
|
|
||||||
<Button
|
|
||||||
className="no-underline text-elements-link"
|
|
||||||
// TODO: use dynamic value
|
|
||||||
href="https://www.iglootools.snowballtools.xyz"
|
|
||||||
as="a"
|
|
||||||
variant="link-emphasized"
|
|
||||||
external
|
|
||||||
leftIcon={<LinkChainIcon />}
|
|
||||||
>
|
|
||||||
{/* // TODO: use dynamic value */}
|
|
||||||
www.iglootools.snowballtools.xyz
|
|
||||||
</Button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Card */}
|
|
||||||
<div className="bg-base-bg-alternate rounded-xl shadow-inset w-full px-1 py-1">
|
|
||||||
{/* Trigger question */}
|
|
||||||
<div className="flex gap-2 justify-center items-center py-3">
|
|
||||||
<div className="h-5 w-5">
|
|
||||||
<QuestionMarkRoundFilledIcon size={18} />
|
|
||||||
</div>
|
</div>
|
||||||
<Heading as="h5" className="font-sans font-medium text-sm">
|
|
||||||
{`Wondering what's next?`}
|
|
||||||
</Heading>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CTA card */}
|
{/* Heading */}
|
||||||
<div className="bg-surface-card rounded-xl shadow-card-sm px-4 py-4">
|
<div className="flex flex-col items-center gap-1.5">
|
||||||
<div className="flex gap-2">
|
<Heading as="h3" className="font-medium text-xl">
|
||||||
<Badge variant="secondary">1</Badge>
|
{isAuction
|
||||||
<div className="space-y-3">
|
? 'Auction created successfully.'
|
||||||
<div className="flex flex-col gap-1">
|
: 'Project deployment created successfully.'}
|
||||||
<Heading as="h6" className="text-sm font-sans">
|
</Heading>
|
||||||
Add a custom domain
|
</div>
|
||||||
</Heading>
|
|
||||||
<p className="text-xs text-elements-low-em font-sans">
|
{/* Card */}
|
||||||
Make it easy for your visitors to remember your URL with a
|
<div className="bg-base-bg-alternate rounded-xl shadow-inset w-full px-1 py-1">
|
||||||
custom domain.
|
{/* Trigger question */}
|
||||||
</p>
|
<div className="flex gap-2 justify-center items-center py-3">
|
||||||
|
<div className="h-5 w-5">
|
||||||
|
<QuestionMarkRoundFilledIcon size={18} />
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleSetupDomain} variant="tertiary" size="sm">
|
<Heading as="h5" className="font-sans font-medium text-sm">
|
||||||
Setup domain
|
{`Wondering what's next?`}
|
||||||
</Button>
|
</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA card */}
|
||||||
|
<div className="bg-surface-card rounded-xl shadow-card-sm px-4 py-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Badge variant="secondary">1</Badge>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Heading as="h6" className="text-sm font-sans">
|
||||||
|
Add a custom domain
|
||||||
|
</Heading>
|
||||||
|
<p className="text-xs text-elements-low-em font-sans">
|
||||||
|
Make it easy for your visitors to remember your URL with a
|
||||||
|
custom domain.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleSetupDomain}
|
||||||
|
variant="tertiary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Setup domain
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Buttons */}
|
||||||
|
<div className="flex flex-col lg:flex-row justify-center gap-3">
|
||||||
|
<div className="w-full lg:w-fit">
|
||||||
|
<Link to="/">
|
||||||
|
<Button
|
||||||
|
leftIcon={<ArrowLeftCircleFilledIcon />}
|
||||||
|
fullWidth
|
||||||
|
variant="tertiary"
|
||||||
|
>
|
||||||
|
Back to projects
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="w-full lg:w-fit">
|
||||||
|
<Link to={`/${orgSlug}/projects/${id}`}>
|
||||||
|
<Button fullWidth variant="primary">
|
||||||
|
View project
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<></>
|
||||||
{/* CTA Buttons */}
|
)}
|
||||||
<div className="flex flex-col lg:flex-row justify-center gap-3">
|
</>
|
||||||
<div className="w-full lg:w-fit">
|
|
||||||
<Link to="/">
|
|
||||||
<Button
|
|
||||||
leftIcon={<ArrowLeftCircleFilledIcon />}
|
|
||||||
fullWidth
|
|
||||||
variant="tertiary"
|
|
||||||
>
|
|
||||||
Back to projects
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="w-full lg:w-fit">
|
|
||||||
<Link to={`/${orgSlug}/projects/${id}`}>
|
|
||||||
<Button fullWidth variant="primary">
|
|
||||||
View project
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
import ConfigureComponent from '../../../../../components/projects/create/Configure';
|
||||||
|
|
||||||
|
const Configure = () => {
|
||||||
|
return <ConfigureComponent />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Configure;
|
@ -6,13 +6,14 @@ import { useMediaQuery } from 'usehooks-ts';
|
|||||||
import { RequestError } from 'octokit';
|
import { RequestError } from 'octokit';
|
||||||
|
|
||||||
import { useOctokit } from '../../../../../context/OctokitContext';
|
import { useOctokit } from '../../../../../context/OctokitContext';
|
||||||
import { useGQLClient } from '../../../../../context/GQLClientContext';
|
|
||||||
import { Template } from '../../../../../types/types';
|
import { Template } from '../../../../../types/types';
|
||||||
import { Heading } from 'components/shared/Heading';
|
import { Heading } from 'components/shared/Heading';
|
||||||
import { Input } from 'components/shared/Input';
|
import { Input } from 'components/shared/Input';
|
||||||
import { Select, SelectOption } from 'components/shared/Select';
|
import { Select, SelectOption } from 'components/shared/Select';
|
||||||
import { ArrowRightCircleFilledIcon } from 'components/shared/CustomIcon';
|
import {
|
||||||
import { Checkbox } from 'components/shared/Checkbox';
|
ArrowRightCircleFilledIcon,
|
||||||
|
LoadingIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import { useToast } from 'components/shared/Toast';
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
|
||||||
@ -28,7 +29,6 @@ type SubmitRepoValues = {
|
|||||||
const CreateRepo = () => {
|
const CreateRepo = () => {
|
||||||
const { octokit, isAuth } = useOctokit();
|
const { octokit, isAuth } = useOctokit();
|
||||||
const { template } = useOutletContext<{ template: Template }>();
|
const { template } = useOutletContext<{ template: Template }>();
|
||||||
const client = useGQLClient();
|
|
||||||
|
|
||||||
const { orgSlug } = useParams();
|
const { orgSlug } = useParams();
|
||||||
const { toast, dismiss } = useToast();
|
const { toast, dismiss } = useToast();
|
||||||
@ -41,6 +41,17 @@ const CreateRepo = () => {
|
|||||||
const [gitAccounts, setGitAccounts] = useState<string[]>([]);
|
const [gitAccounts, setGitAccounts] = useState<string[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const checkRepoExists = async (account: string, repoName: string) => {
|
||||||
|
try {
|
||||||
|
await octokit.rest.repos.get({ owner: account, repo: repoName });
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// Error handled by octokit error hook interceptor in Octokit context
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitRepoHandler: SubmitHandler<SubmitRepoValues> = useCallback(
|
const submitRepoHandler: SubmitHandler<SubmitRepoValues> = useCallback(
|
||||||
async (data) => {
|
async (data) => {
|
||||||
assert(data.account);
|
assert(data.account);
|
||||||
@ -50,36 +61,23 @@ const CreateRepo = () => {
|
|||||||
assert(template.repoFullName, 'Template URL not provided');
|
assert(template.repoFullName, 'Template URL not provided');
|
||||||
const [owner, repo] = template.repoFullName.split('/');
|
const [owner, repo] = template.repoFullName.split('/');
|
||||||
|
|
||||||
// TODO: Handle this functionality in backend
|
const repoExists = await checkRepoExists(data.account, data.repoName);
|
||||||
const gitRepo = await octokit?.rest.repos.createUsingTemplate({
|
if (repoExists) {
|
||||||
template_owner: owner,
|
toast({
|
||||||
template_repo: repo,
|
id: 'repo-exist-error',
|
||||||
owner: data.account,
|
title: 'Repository already exists with this name',
|
||||||
name: data.repoName,
|
variant: 'warning',
|
||||||
include_all_branches: false,
|
onDismiss: dismiss,
|
||||||
private: data.isPrivate,
|
});
|
||||||
});
|
setIsLoading(false);
|
||||||
|
|
||||||
if (!gitRepo) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refetch to always get correct default branch
|
setIsLoading(true);
|
||||||
const templateRepo = await octokit.rest.repos.get({
|
|
||||||
owner: template.repoFullName.split('/')[0],
|
|
||||||
repo: template.repoFullName.split('/')[1],
|
|
||||||
});
|
|
||||||
const prodBranch = templateRepo.data.default_branch ?? 'main';
|
|
||||||
|
|
||||||
const { addProject } = await client.addProject(orgSlug!, {
|
navigate(
|
||||||
name: `${gitRepo.data.owner!.login}-${gitRepo.data.name}`,
|
`configure?templateId=${template.id}&templateOwner=${owner}&templateRepo=${repo}&owner=${data.account}&name=${data.repoName}&isPrivate=false&orgSlug=${orgSlug}`,
|
||||||
prodBranch,
|
);
|
||||||
repository: gitRepo.data.full_name,
|
|
||||||
// TODO: Set selected template
|
|
||||||
template: 'webapp',
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(`deploy?projectId=${addProject.id}&templateId=${template.id}`);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
@ -106,7 +104,7 @@ const CreateRepo = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[octokit],
|
[octokit, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -187,28 +185,26 @@ const CreateRepo = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="repoName"
|
name="repoName"
|
||||||
control={control}
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<Input value={value} onChange={onChange} />
|
<Input value={value} onChange={onChange} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Controller
|
|
||||||
name="isPrivate"
|
|
||||||
control={control}
|
|
||||||
render={({}) => (
|
|
||||||
<Checkbox label="Make this repo private" disabled={true} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
{...buttonSize}
|
{...buttonSize}
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!Boolean(template.repoFullName) || isLoading}
|
disabled={!Boolean(template.repoFullName) || isLoading}
|
||||||
rightIcon={<ArrowRightCircleFilledIcon />}
|
rightIcon={
|
||||||
|
isLoading ? (
|
||||||
|
<LoadingIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowRightCircleFilledIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Deploy
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user