Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af1137b3c3 | ||
|
|
a7810a34c9 | ||
|
|
ff9b08e91d | ||
|
|
94a9bf88a9 | ||
|
|
dd5f93050d | ||
|
|
5946755749 | ||
|
|
4532a962d8 | ||
|
|
6a108c1a1b | ||
|
|
0fb378386a | ||
|
|
563059e887 | ||
|
|
f34eb48d1e | ||
|
|
acf5aab26f | ||
|
|
6b6582c287 | ||
|
|
b43ee3b7bb | ||
|
|
b1bf47d104 | ||
|
|
8ee61c0c85 | ||
|
|
33aa75e341 | ||
|
|
a238610522 | ||
|
|
bd4a51c830 | ||
|
|
4a87c100d6 | ||
|
|
3493d735b9 | ||
|
|
a324d32ebf | ||
|
|
4519494be8 | ||
|
|
319822c8e6 | ||
|
|
c5273ff530 | ||
|
|
2585418ed6 | ||
|
|
62734308fc | ||
|
|
89306ddcc7 | ||
|
|
3b67396c43 | ||
|
|
3535bd3a58 | ||
|
|
282001c317 | ||
|
|
2f62d086de | ||
|
|
4c544e8d15 | ||
|
|
a02ee13f9d | ||
|
|
e32a51a771 | ||
|
|
513ca69d01 | ||
|
|
e34b8d6a2e | ||
|
|
f4d5b77784 | ||
|
|
399b80ce3b | ||
|
|
fdaec2da94 | ||
|
|
13fc92bf0e | ||
|
|
8a8ee1d6ae | ||
|
|
5680961964 | ||
|
|
aa0ca8dd21 | ||
|
|
8280f4c7f7 | ||
|
|
ab72bbb03f | ||
|
|
cc4e5fd627 | ||
|
|
195471b888 | ||
|
|
c731dd308c | ||
|
|
8ce531274d | ||
|
|
6c38094093 | ||
|
|
95285d30dd | ||
|
|
61c8846400 | ||
|
|
d73c3c7daf | ||
|
|
effa0bbf14 | ||
|
|
10a875d6e8 | ||
|
|
284eb1403a | ||
|
|
538e89e3c9 | ||
|
|
b88752da69 | ||
|
|
48c569c371 | ||
|
|
549dc5af10 | ||
|
|
012beaad57 | ||
|
|
81f27442fb | ||
|
|
bdb948f69e | ||
|
|
2442e92395 | ||
|
|
dcf271a01b | ||
|
|
48fd953c60 | ||
|
|
5eff273abb | ||
|
|
f9af066333 | ||
|
|
04460a24ac | ||
|
|
ccc4589033 | ||
|
|
cf2f9a88e1 | ||
|
|
50749055bf | ||
|
|
ef7c673698 | ||
|
|
7f11d8818d | ||
|
|
ece9a2ecec | ||
|
|
cb614c8d8f | ||
|
|
dbb928cd2f | ||
|
|
cfd129fb96 | ||
|
|
efc10d3d7e | ||
|
|
042aff2f87 | ||
|
|
b48afed24f | ||
|
|
0dfecd024d | ||
|
|
6126c03eee | ||
|
|
35be62b28e | ||
|
|
9db87e6be3 | ||
|
|
353fd0f621 | ||
|
|
5a5756bcf2 | ||
|
|
22233d95f8 | ||
|
|
12fba5ee0c | ||
|
|
9b6c777f5f | ||
|
|
3718e260f5 | ||
|
|
05e1ac6b02 | ||
|
|
55848b1dd1 | ||
|
|
42bf1534fb | ||
|
|
9acb9daacc | ||
|
|
f7c45ca045 | ||
|
|
ef0eac8293 | ||
|
|
7d1810ebd9 | ||
|
|
d2ca4df35a | ||
|
|
30bbe4d766 | ||
|
|
0e7343da6f | ||
|
|
8952393b31 | ||
|
|
32918b7930 | ||
|
|
596889b5e2 | ||
|
|
fb932eeb04 | ||
|
|
200ea3ca7b | ||
|
|
ba87c6f22a | ||
|
|
4d646e1ac0 | ||
|
|
5be29a9745 | ||
|
|
fffc370d40 | ||
|
|
1cd60d84dd | ||
|
|
a846531e43 | ||
|
|
c8a153ad27 | ||
|
|
bb7a88f7e9 | ||
|
|
675112079c | ||
|
|
cb928c3360 | ||
|
|
5836779403 | ||
|
|
028dd35db1 | ||
|
|
ef9f4df28a | ||
|
|
6b17dce2ae | ||
|
|
496404195c | ||
|
|
9dec48d67c | ||
|
|
267b52a352 | ||
|
|
fa96b6e734 | ||
|
|
22c581dd33 | ||
|
|
d3013719e6 | ||
|
|
98a4d7be07 | ||
|
|
3928e2610f | ||
|
|
075cc05db5 | ||
|
|
e816c596ca | ||
|
|
6df094bf2e | ||
|
|
4d0f2ca893 | ||
|
|
8ca55cd888 | ||
|
|
fc240c93d8 | ||
|
|
a45fb4c617 | ||
|
|
c3d1b4f3eb | ||
|
|
ce55fe62d8 | ||
|
|
b007286f4a |
@@ -1 +1,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
yarn-error.log
|
||||||
|
.yarnrc.yml
|
||||||
|
.yarn/
|
||||||
|
.yarnrc
|
||||||
|
|
||||||
|
packages/backend/environments/local.toml
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# snowballtools
|
# snowballtools-base
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
- Clone the `snowballtools` repo
|
- Clone the `snowballtools-base` repo
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone git@github.com:snowball-tools/snowballtools-base.git
|
git clone git@github.com:snowball-tools/snowballtools-base.git
|
||||||
@@ -28,63 +28,144 @@
|
|||||||
cd packages/backend
|
cd packages/backend
|
||||||
```
|
```
|
||||||
|
|
||||||
- Load fixtures in database
|
- Rename backend config file from [environments/local.toml.example](packages/backend/environments/local.toml.example) to `local.toml`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn test:db:load:fixtures
|
mv environments/local.toml.example environments/local.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
||||||
- Client ID and secret will be available after creating Github OAuth app
|
- Client ID and secret will be available after [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
||||||
- https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app
|
|
||||||
- In "Homepage URL", type `http://localhost:3000`
|
- In "Homepage URL", type `http://localhost:3000`
|
||||||
- In "Authorization callback URL", type `http://localhost:3000/organization/projects/create`
|
- In "Authorization callback URL", type `http://localhost:3000/organization/projects/create`
|
||||||
- Generate a new client secret after app is created
|
- Generate a new client secret after app is created
|
||||||
|
|
||||||
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
|
### Backend Production
|
||||||
|
|
||||||
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
|
- Let us assume the following domains for backend and frontend
|
||||||
|
- Backend server: `api.snowballtools.com`
|
||||||
|
- Frontend app: `dashboard.snowballtools.com`
|
||||||
|
|
||||||
```bash
|
- Update the following in backend [config file](packages/backend/environments/local.toml)
|
||||||
laconic-so --stack fixturenet-laconic-loaded deploy 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
|
```toml
|
||||||
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
|
[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 = ""
|
||||||
```
|
```
|
||||||
|
|
||||||
- Get the REST and GQL endpoint ports of Laconicd and replace the ports for `registryConfig.restEndpoint` and `registryConfig.gqlEndpoint` in backend [config file](packages/backend/environments/local.toml)
|
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
||||||
|
- Client ID and secret will be available after [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
||||||
|
- In "Homepage URL", type `https://dashboard.snowballtools.com`
|
||||||
|
- In "Authorization callback URL", type `https://dashboard.snowballtools.com/organization/projects/create`
|
||||||
|
- Generate a new client secret after app is created
|
||||||
|
|
||||||
```bash
|
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
||||||
# For registryConfig.restEndpoint
|
|
||||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
|
|
||||||
# 0.0.0.0:32777
|
|
||||||
|
|
||||||
# For registryConfig.gqlEndpoint
|
```toml
|
||||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
|
...
|
||||||
# 0.0.0.0:32771
|
[gitHub]
|
||||||
|
webhookUrl = "https://api.snowballtools.com"
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
- Run the script to create bond, reserve the authority and set authority bond
|
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
|
||||||
|
|
||||||
```bash
|
```toml
|
||||||
yarn test:registry:init
|
...
|
||||||
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
|
[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"
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
|
- Start the server in `packages/backend`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Development
|
||||||
|
|
||||||
|
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
||||||
|
- Client ID and secret will be available after [creating an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
||||||
|
- In "Homepage URL", type `http://localhost:3000`
|
||||||
|
- In "Authorization callback URL", type `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 --stack fixturenet-laconic-loaded deploy 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
|
||||||
|
```
|
||||||
|
|
||||||
|
- Get the REST and GQL endpoint ports of Laconicd and replace the ports for `registryConfig.restEndpoint` and `registryConfig.gqlEndpoint` in backend [config file](packages/backend/environments/local.toml)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For registryConfig.restEndpoint
|
||||||
|
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
|
||||||
|
# 0.0.0.0:32777
|
||||||
|
|
||||||
|
# For registryConfig.gqlEndpoint
|
||||||
|
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
|
||||||
|
# 0.0.0.0:32771
|
||||||
|
```
|
||||||
|
|
||||||
|
- 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
|
- Setup ngrok for GitHub webhooks
|
||||||
- https://ngrok.com/docs/getting-started/
|
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
|
||||||
- Start ngrok and point to backend server endpoint
|
- Start ngrok and point to backend server endpoint
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ngrok http http://localhost:8000
|
ngrok http http://localhost:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
- Look for the forwarding URL in ngrok
|
- Look for the forwarding URL in ngrok
|
||||||
```
|
|
||||||
|
```bash
|
||||||
...
|
...
|
||||||
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
|
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)
|
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
...
|
...
|
||||||
[gitHub]
|
[gitHub]
|
||||||
@@ -95,7 +176,7 @@
|
|||||||
- Start the server in `packages/backend`
|
- Start the server in `packages/backend`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn start
|
yarn start:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Frontend
|
## Frontend
|
||||||
@@ -106,10 +187,10 @@
|
|||||||
cd packages/frontend
|
cd packages/frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
|
- Rename [.env.example](packages/frontend/.env.example) to `.env`
|
||||||
|
|
||||||
```env
|
```bash
|
||||||
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
|
mv .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
|
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
|
||||||
@@ -118,23 +199,36 @@
|
|||||||
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
|
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
|
||||||
```
|
```
|
||||||
|
|
||||||
- Set `REACT_APP_GITHUB_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
- Set `REACT_APP_GITHUB_PWA_TEMPLATE_REPO` and `REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
||||||
|
|
||||||
```env
|
```env
|
||||||
REACT_APP_GITHUB_TEMPLATE_REPO = cerc-io/test-progressive-web-app
|
# Set actual owner/name of the template repo that will be used for creating new repo
|
||||||
|
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = cerc-io/test-progressive-web-app
|
||||||
|
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = cerc-io/image-upload-pwa-example
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development
|
### Frontend Production
|
||||||
|
|
||||||
- Start the React application
|
- Let us assume the following domains for backend and frontend
|
||||||
|
- Backend server: `api.snowballtools.com`
|
||||||
|
- Frontend app: `dashboard.snowballtools.com`
|
||||||
|
|
||||||
```bash
|
- Set the following values in [.env](packages/frontend/.env) file
|
||||||
yarn start
|
|
||||||
|
```env
|
||||||
|
# Backend server endpoint
|
||||||
|
REACT_APP_SERVER_URL = 'https://api.snowballtools.com'
|
||||||
```
|
```
|
||||||
|
|
||||||
- The React application will be running in `http://localhost:3000/`
|
- 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 `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
||||||
|
|
||||||
### Production
|
```env
|
||||||
|
REACT_APP_WALLET_CONNECT_ID = <PROJECT_ID>
|
||||||
|
```
|
||||||
|
|
||||||
- Build the React application
|
- Build the React application
|
||||||
|
|
||||||
@@ -148,3 +242,28 @@
|
|||||||
python3 -m http.server -d build 3000
|
python3 -m http.server -d build 3000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Frontend Development
|
||||||
|
|
||||||
|
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
|
||||||
|
|
||||||
|
```env
|
||||||
|
REACT_APP_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 `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
||||||
|
|
||||||
|
```env
|
||||||
|
REACT_APP_WALLET_CONNECT_ID = <Project_ID>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Start the React application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
- The React application will be running in `http://localhost:3000/`
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PKG_DIR="./packages/frontend"
|
||||||
|
OUTPUT_DIR="${PKG_DIR}/build"
|
||||||
|
DEST_DIR=${1:-/data}
|
||||||
|
|
||||||
|
if [[ -d "$DEST_DIR" ]]; then
|
||||||
|
echo "${DEST_DIR} already exists." 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > $PKG_DIR/.env <<EOF
|
||||||
|
REACT_APP_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_server_url'
|
||||||
|
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
|
||||||
|
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo'
|
||||||
|
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo'
|
||||||
|
REACT_APP_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_app_wallet_connect_id'
|
||||||
|
EOF
|
||||||
|
|
||||||
|
yarn || exit 1
|
||||||
|
yarn build || exit 1
|
||||||
|
|
||||||
|
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
||||||
|
echo "Missing output directory: $OUTPUT_DIR" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$OUTPUT_DIR" "$DEST_DIR"
|
||||||
@@ -25,6 +25,9 @@
|
|||||||
"allowArgumentsExplicitlyTypedAsAny": true
|
"allowArgumentsExplicitlyTypedAsAny": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{ "ignoreRestSiblings": true }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
db
|
db
|
||||||
dist
|
dist
|
||||||
|
environments/local.toml
|
||||||
@@ -2,6 +2,11 @@
|
|||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = 8000
|
port = 8000
|
||||||
gqlPath = "/graphql"
|
gqlPath = "/graphql"
|
||||||
|
[server.session]
|
||||||
|
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi"
|
||||||
|
appOriginUrl = "http://localhost:3000"
|
||||||
|
trustProxy = false
|
||||||
|
domain = "localhost"
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
dbPath = "db/snowball"
|
dbPath = "db/snowball"
|
||||||
@@ -19,7 +24,11 @@
|
|||||||
chainId = "laconic_9000-1"
|
chainId = "laconic_9000-1"
|
||||||
privateKey = ""
|
privateKey = ""
|
||||||
bondId = ""
|
bondId = ""
|
||||||
|
authority = ""
|
||||||
[registryConfig.fee]
|
[registryConfig.fee]
|
||||||
amount = "200000"
|
amount = "200000"
|
||||||
denom = "aphoton"
|
denom = "aphoton"
|
||||||
gas = "550000"
|
gas = "750000"
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
projectDomain = "apps.snowballtools.com"
|
||||||
@@ -12,8 +12,10 @@
|
|||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
"apollo-server-core": "^3.13.0",
|
"apollo-server-core": "^3.13.0",
|
||||||
"apollo-server-express": "^3.13.0",
|
"apollo-server-express": "^3.13.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-session": "^1.18.0",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
@@ -42,6 +44,7 @@
|
|||||||
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
|
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/express-session": "^1.17.10",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||||
"@typescript-eslint/parser": "^6.18.1",
|
"@typescript-eslint/parser": "^6.18.1",
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
|
export interface SessionConfig {
|
||||||
|
secret: string;
|
||||||
|
appOriginUrl: string;
|
||||||
|
trustProxy: boolean;
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerConfig {
|
export interface ServerConfig {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
gqlPath?: string;
|
gqlPath?: string;
|
||||||
|
sessionSecret: string;
|
||||||
|
appOriginUrl: string;
|
||||||
|
isProduction: boolean;
|
||||||
|
session: SessionConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseConfig {
|
export interface DatabaseConfig {
|
||||||
@@ -13,7 +24,7 @@ export interface GitHubConfig {
|
|||||||
oAuth: {
|
oAuth: {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegistryConfig {
|
export interface RegistryConfig {
|
||||||
@@ -23,11 +34,16 @@ export interface RegistryConfig {
|
|||||||
privateKey: string;
|
privateKey: string;
|
||||||
bondId: string;
|
bondId: string;
|
||||||
fetchDeploymentRecordDelay: number;
|
fetchDeploymentRecordDelay: number;
|
||||||
|
authority: string;
|
||||||
fee: {
|
fee: {
|
||||||
amount: string;
|
amount: string;
|
||||||
denom: string;
|
denom: string;
|
||||||
gas: string;
|
gas: string;
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MiscConfig {
|
||||||
|
projectDomain: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
@@ -35,4 +51,5 @@ export interface Config {
|
|||||||
database: DatabaseConfig;
|
database: DatabaseConfig;
|
||||||
gitHub: GitHubConfig;
|
gitHub: GitHubConfig;
|
||||||
registryConfig: RegistryConfig;
|
registryConfig: RegistryConfig;
|
||||||
|
misc: MiscConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
export const DEFAULT_CONFIG_FILE_PATH = 'environments/local.toml';
|
import process from 'process';
|
||||||
|
|
||||||
|
export const DEFAULT_CONFIG_FILE_PATH =
|
||||||
|
process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml';
|
||||||
|
|
||||||
export const DEFAULT_GQL_PATH = '/graphql';
|
export const DEFAULT_GQL_PATH = '/graphql';
|
||||||
|
|
||||||
// Note: temporary hardcoded user, later to be derived from auth token
|
|
||||||
export const USER_ID = '59f4355d-9549-4aac-9b54-eeefceeabef0';
|
|
||||||
|
|
||||||
export const PROJECT_DOMAIN = 'snowball.xyz';
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { DataSource, DeepPartial, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm';
|
import {
|
||||||
|
DataSource,
|
||||||
|
DeepPartial,
|
||||||
|
FindManyOptions,
|
||||||
|
FindOneOptions,
|
||||||
|
FindOptionsWhere
|
||||||
|
} from 'typeorm';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import assert from 'assert';
|
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 } from './config';
|
import { DatabaseConfig, MiscConfig } 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';
|
||||||
@@ -13,7 +19,10 @@ import { Deployment } from './entity/Deployment';
|
|||||||
import { ProjectMember } from './entity/ProjectMember';
|
import { ProjectMember } from './entity/ProjectMember';
|
||||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||||
import { Domain } from './entity/Domain';
|
import { Domain } from './entity/Domain';
|
||||||
import { PROJECT_DOMAIN } from './constants';
|
import { getEntities, loadAndSaveData } from './utils';
|
||||||
|
import { UserOrganization } from './entity/UserOrganization';
|
||||||
|
|
||||||
|
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
||||||
|
|
||||||
const log = debug('snowball:database');
|
const log = debug('snowball:database');
|
||||||
|
|
||||||
@@ -22,8 +31,9 @@ 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) {
|
constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) {
|
||||||
this.dataSource = new DataSource({
|
this.dataSource = new DataSource({
|
||||||
type: 'better-sqlite3',
|
type: 'better-sqlite3',
|
||||||
database: dbPath,
|
database: dbPath,
|
||||||
@@ -31,11 +41,21 @@ 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');
|
||||||
|
|
||||||
|
const organizations = await this.getOrganizations({});
|
||||||
|
|
||||||
|
// Load an organization if none exist
|
||||||
|
if (!organizations.length) {
|
||||||
|
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
||||||
|
await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser (options: FindOneOptions<User>): Promise<User | null> {
|
async getUser (options: FindOneOptions<User>): Promise<User | null> {
|
||||||
@@ -52,15 +72,26 @@ export class Database {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser (userId: string, 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: userId }, data);
|
const updateResult = await userRepository.update({ id: user.id }, data);
|
||||||
assert(updateResult.affected);
|
assert(updateResult.affected);
|
||||||
|
|
||||||
return updateResult.affected > 0;
|
return updateResult.affected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> {
|
async getOrganizations (
|
||||||
|
options: FindManyOptions<Organization>
|
||||||
|
): Promise<Organization[]> {
|
||||||
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
|
const organizations = await organizationRepository.find(options);
|
||||||
|
|
||||||
|
return organizations;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganization (
|
||||||
|
options: FindOneOptions<Organization>
|
||||||
|
): Promise<Organization | null> {
|
||||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
const organization = await organizationRepository.findOne(options);
|
const organization = await organizationRepository.findOne(options);
|
||||||
|
|
||||||
@@ -83,6 +114,13 @@ export class Database {
|
|||||||
return userOrgs;
|
return userOrgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addUserOrganization (data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
|
||||||
|
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
|
||||||
|
const newUserOrganization = await userOrganizationRepository.save(data);
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -95,7 +133,11 @@ export class Database {
|
|||||||
|
|
||||||
const project = await projectRepository
|
const project = await projectRepository
|
||||||
.createQueryBuilder('project')
|
.createQueryBuilder('project')
|
||||||
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
.leftJoinAndSelect(
|
||||||
|
'project.deployments',
|
||||||
|
'deployments',
|
||||||
|
'deployments.isCurrent = true'
|
||||||
|
)
|
||||||
.leftJoinAndSelect('deployments.createdBy', 'user')
|
.leftJoinAndSelect('deployments.createdBy', 'user')
|
||||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||||
.leftJoinAndSelect('project.owner', 'owner')
|
.leftJoinAndSelect('project.owner', 'owner')
|
||||||
@@ -108,19 +150,29 @@ export class Database {
|
|||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsInOrganization (userId: string, organizationSlug: string): Promise<Project[]> {
|
async getProjectsInOrganization (
|
||||||
|
userId: string,
|
||||||
|
organizationSlug: string
|
||||||
|
): Promise<Project[]> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
const projects = await projectRepository
|
const projects = await projectRepository
|
||||||
.createQueryBuilder('project')
|
.createQueryBuilder('project')
|
||||||
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
.leftJoinAndSelect(
|
||||||
|
'project.deployments',
|
||||||
|
'deployments',
|
||||||
|
'deployments.isCurrent = true'
|
||||||
|
)
|
||||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||||
.leftJoin('project.projectMembers', 'projectMembers')
|
.leftJoin('project.projectMembers', 'projectMembers')
|
||||||
.leftJoin('project.organization', 'organization')
|
.leftJoin('project.organization', 'organization')
|
||||||
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', {
|
.where(
|
||||||
userId,
|
'(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug',
|
||||||
organizationSlug
|
{
|
||||||
})
|
userId,
|
||||||
|
organizationSlug
|
||||||
|
}
|
||||||
|
)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
@@ -129,7 +181,9 @@ export class Database {
|
|||||||
/**
|
/**
|
||||||
* Get deployments with specified filter
|
* Get deployments with specified filter
|
||||||
*/
|
*/
|
||||||
async getDeployments (options: FindManyOptions<Deployment>): Promise<Deployment[]> {
|
async getDeployments (
|
||||||
|
options: FindManyOptions<Deployment>
|
||||||
|
): Promise<Deployment[]> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
const deployments = await deploymentRepository.find(options);
|
const deployments = await deploymentRepository.find(options);
|
||||||
|
|
||||||
@@ -154,7 +208,9 @@ export class Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeployment (options: FindOneOptions<Deployment>): Promise<Deployment | null> {
|
async getDeployment (
|
||||||
|
options: FindOneOptions<Deployment>
|
||||||
|
): Promise<Deployment | null> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
const deployment = await deploymentRepository.findOne(options);
|
const deployment = await deploymentRepository.findOne(options);
|
||||||
|
|
||||||
@@ -182,8 +238,11 @@ export class Database {
|
|||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
|
async getProjectMembersByProjectId (
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
projectId: string
|
||||||
|
): Promise<ProjectMember[]> {
|
||||||
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
const projectMembers = await projectMemberRepository.find({
|
const projectMembers = await projectMemberRepository.find({
|
||||||
relations: {
|
relations: {
|
||||||
@@ -200,8 +259,12 @@ export class Database {
|
|||||||
return projectMembers;
|
return projectMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEnvironmentVariablesByProjectId (projectId: string, filter?: FindOptionsWhere<EnvironmentVariable>): Promise<EnvironmentVariable[]> {
|
async getEnvironmentVariablesByProjectId (
|
||||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
projectId: string,
|
||||||
|
filter?: FindOptionsWhere<EnvironmentVariable>
|
||||||
|
): Promise<EnvironmentVariable[]> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable);
|
||||||
|
|
||||||
const environmentVariables = await environmentVariableRepository.find({
|
const environmentVariables = await environmentVariableRepository.find({
|
||||||
where: {
|
where: {
|
||||||
@@ -216,9 +279,12 @@ export class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
|
async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
const deleteResult = await projectMemberRepository.delete({ id: projectMemberId });
|
const deleteResult = await projectMemberRepository.delete({
|
||||||
|
id: projectMemberId
|
||||||
|
});
|
||||||
|
|
||||||
if (deleteResult.affected) {
|
if (deleteResult.affected) {
|
||||||
return deleteResult.affected > 0;
|
return deleteResult.affected > 0;
|
||||||
@@ -227,37 +293,63 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectMemberById (projectMemberId: string, data: DeepPartial<ProjectMember>): Promise<boolean> {
|
async updateProjectMemberById (
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
projectMemberId: string,
|
||||||
const updateResult = await projectMemberRepository.update({ id: projectMemberId }, data);
|
data: DeepPartial<ProjectMember>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
const updateResult = await projectMemberRepository.update(
|
||||||
|
{ id: projectMemberId },
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProjectMember (data: DeepPartial<ProjectMember>): Promise<ProjectMember> {
|
async addProjectMember (
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
data: DeepPartial<ProjectMember>
|
||||||
|
): Promise<ProjectMember> {
|
||||||
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
const newProjectMember = await projectMemberRepository.save(data);
|
const newProjectMember = await projectMemberRepository.save(data);
|
||||||
|
|
||||||
return newProjectMember;
|
return newProjectMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addEnvironmentVariables (data: DeepPartial<EnvironmentVariable>[]): Promise<EnvironmentVariable[]> {
|
async addEnvironmentVariables (
|
||||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
data: DeepPartial<EnvironmentVariable>[]
|
||||||
const savedEnvironmentVariables = await environmentVariableRepository.save(data);
|
): Promise<EnvironmentVariable[]> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable);
|
||||||
|
const savedEnvironmentVariables =
|
||||||
|
await environmentVariableRepository.save(data);
|
||||||
|
|
||||||
return savedEnvironmentVariables;
|
return savedEnvironmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEnvironmentVariable (environmentVariableId: string, data: DeepPartial<EnvironmentVariable>): Promise<boolean> {
|
async updateEnvironmentVariable (
|
||||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
environmentVariableId: string,
|
||||||
const updateResult = await environmentVariableRepository.update({ id: environmentVariableId }, data);
|
data: DeepPartial<EnvironmentVariable>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable);
|
||||||
|
const updateResult = await environmentVariableRepository.update(
|
||||||
|
{ id: environmentVariableId },
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
async deleteEnvironmentVariable (
|
||||||
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
|
environmentVariableId: string
|
||||||
const deleteResult = await environmentVariableRepository.delete({ id: environmentVariableId });
|
): Promise<boolean> {
|
||||||
|
const environmentVariableRepository =
|
||||||
|
this.dataSource.getRepository(EnvironmentVariable);
|
||||||
|
const deleteResult = await environmentVariableRepository.delete({
|
||||||
|
id: environmentVariableId
|
||||||
|
});
|
||||||
|
|
||||||
if (deleteResult.affected) {
|
if (deleteResult.affected) {
|
||||||
return deleteResult.affected > 0;
|
return deleteResult.affected > 0;
|
||||||
@@ -267,7 +359,8 @@ export class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
|
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
|
||||||
const projectMemberRepository = this.dataSource.getRepository(ProjectMember);
|
const projectMemberRepository =
|
||||||
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
const projectMemberWithProject = await projectMemberRepository.find({
|
const projectMemberWithProject = await projectMemberRepository.find({
|
||||||
relations: {
|
relations: {
|
||||||
@@ -279,8 +372,7 @@ export class Database {
|
|||||||
where: {
|
where: {
|
||||||
id: projectMemberId
|
id: projectMemberId
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (projectMemberWithProject.length === 0) {
|
if (projectMemberWithProject.length === 0) {
|
||||||
throw new Error('Member does not exist');
|
throw new Error('Member does not exist');
|
||||||
@@ -289,34 +381,49 @@ export class Database {
|
|||||||
return projectMemberWithProject[0];
|
return projectMemberWithProject[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsBySearchText (userId: string, searchText: string): Promise<Project[]> {
|
async getProjectsBySearchText (
|
||||||
|
userId: string,
|
||||||
|
searchText: string
|
||||||
|
): Promise<Project[]> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
const projects = await projectRepository
|
const projects = await projectRepository
|
||||||
.createQueryBuilder('project')
|
.createQueryBuilder('project')
|
||||||
.leftJoinAndSelect('project.organization', 'organization')
|
.leftJoinAndSelect('project.organization', 'organization')
|
||||||
.leftJoin('project.projectMembers', 'projectMembers')
|
.leftJoin('project.projectMembers', 'projectMembers')
|
||||||
.where('(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText', {
|
.where(
|
||||||
userId,
|
'(project.owner = :userId OR projectMembers.member.id = :userId) AND project.name LIKE :searchText',
|
||||||
searchText: `%${searchText}%`
|
{
|
||||||
})
|
userId,
|
||||||
|
searchText: `%${searchText}%`
|
||||||
|
}
|
||||||
|
)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentById (deploymentId: string, data: DeepPartial<Deployment>): Promise<boolean> {
|
async updateDeploymentById (
|
||||||
|
deploymentId: string,
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
return this.updateDeployment({ id: deploymentId }, data);
|
return this.updateDeployment({ id: deploymentId }, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeployment (criteria: FindOptionsWhere<Deployment>, data: DeepPartial<Deployment>): Promise<boolean> {
|
async updateDeployment (
|
||||||
|
criteria: FindOptionsWhere<Deployment>,
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
const updateResult = await deploymentRepository.update(criteria, data);
|
const updateResult = await deploymentRepository.update(criteria, data);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentsByProjectIds (projectIds: string[], data: DeepPartial<Deployment>): Promise<boolean> {
|
async updateDeploymentsByProjectIds (
|
||||||
|
projectIds: string[],
|
||||||
|
data: DeepPartial<Deployment>
|
||||||
|
): Promise<boolean> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
|
|
||||||
const updateResult = await deploymentRepository
|
const updateResult = await deploymentRepository
|
||||||
@@ -329,7 +436,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProject (userId: string, 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
|
||||||
@@ -339,22 +446,26 @@ export class Database {
|
|||||||
// TODO: Set icon according to framework
|
// TODO: Set icon according to framework
|
||||||
newProject.icon = '';
|
newProject.icon = '';
|
||||||
|
|
||||||
newProject.owner = Object.assign(new User(), {
|
newProject.owner = user;
|
||||||
id: userId
|
|
||||||
});
|
|
||||||
|
|
||||||
newProject.organization = Object.assign(new Organization(), {
|
newProject.organization = Object.assign(new Organization(), {
|
||||||
id: organizationId
|
id: organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
newProject.subDomain = `${newProject.name}.${PROJECT_DOMAIN}`;
|
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
|
||||||
|
|
||||||
return projectRepository.save(newProject);
|
return projectRepository.save(newProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectById (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
|
async updateProjectById (
|
||||||
|
projectId: string,
|
||||||
|
data: DeepPartial<Project>
|
||||||
|
): Promise<boolean> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
const updateResult = await projectRepository.update({ id: projectId }, data);
|
const updateResult = await projectRepository.update(
|
||||||
|
{ id: projectId },
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
@@ -401,14 +512,20 @@ export class Database {
|
|||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDomainById (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
|
async updateDomainById (
|
||||||
|
domainId: string,
|
||||||
|
data: DeepPartial<Domain>
|
||||||
|
): Promise<boolean> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
const updateResult = await domainRepository.update({ id: domainId }, data);
|
const updateResult = await domainRepository.update({ id: domainId }, data);
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
|
async getDomainsByProjectId (
|
||||||
|
projectId: string,
|
||||||
|
filter?: FindOptionsWhere<Domain>
|
||||||
|
): Promise<Domain[]> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
|
|
||||||
const domains = await domainRepository.find({
|
const domains = await domainRepository.find({
|
||||||
|
|||||||
@@ -26,18 +26,27 @@ export enum DeploymentStatus {
|
|||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApplicationDeploymentRequest {
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
name: string;
|
||||||
|
application: string;
|
||||||
|
config: string;
|
||||||
|
meta: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApplicationRecord {
|
export interface ApplicationRecord {
|
||||||
type: string;
|
type: string;
|
||||||
version:string
|
version: string;
|
||||||
name: string
|
name: string;
|
||||||
description?: string
|
description?: string;
|
||||||
homepage?: string
|
homepage?: string;
|
||||||
license?: string
|
license?: string;
|
||||||
author?: string
|
author?: string;
|
||||||
repository?: string[],
|
repository?: string[];
|
||||||
app_version?: string
|
app_version?: string;
|
||||||
repository_ref: string
|
repository_ref: string;
|
||||||
app_type: string
|
app_type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@@ -78,6 +87,12 @@ export class Deployment {
|
|||||||
@Column('simple-json')
|
@Column('simple-json')
|
||||||
applicationRecordData!: ApplicationRecord;
|
applicationRecordData!: ApplicationRecord;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
applicationDeploymentRequestId!: string | null;
|
||||||
|
|
||||||
|
@Column('simple-json', { nullable: true })
|
||||||
|
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null;
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
applicationDeploymentRecordId!: string | null;
|
applicationDeploymentRecordId!: string | null;
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export class Domain {
|
|||||||
|
|
||||||
@ManyToOne(() => Domain)
|
@ManyToOne(() => Domain)
|
||||||
@JoinColumn({ name: 'redirectToId' })
|
@JoinColumn({ name: 'redirectToId' })
|
||||||
// eslint-disable-next-line no-use-before-define
|
// eslint-disable-next-line no-use-before-define
|
||||||
redirectTo!: Domain | null;
|
redirectTo!: Domain | null;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
|
|||||||
@@ -27,8 +27,12 @@ export class Organization {
|
|||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@OneToMany(() => UserOrganization, userOrganization => userOrganization.organization, {
|
@OneToMany(
|
||||||
cascade: ['soft-remove']
|
() => UserOrganization,
|
||||||
})
|
(userOrganization) => userOrganization.organization,
|
||||||
|
{
|
||||||
|
cascade: ['soft-remove']
|
||||||
|
}
|
||||||
|
)
|
||||||
userOrganizations!: UserOrganization[];
|
userOrganizations!: UserOrganization[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,6 @@ import { Organization } from './Organization';
|
|||||||
import { ProjectMember } from './ProjectMember';
|
import { ProjectMember } from './ProjectMember';
|
||||||
import { Deployment } from './Deployment';
|
import { Deployment } from './Deployment';
|
||||||
|
|
||||||
export interface ApplicationDeploymentRequest {
|
|
||||||
type: string
|
|
||||||
version: string
|
|
||||||
name: string
|
|
||||||
application: string
|
|
||||||
config: string,
|
|
||||||
meta: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Project {
|
export class Project {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
@@ -52,12 +43,6 @@ export class Project {
|
|||||||
@Column('varchar', { length: 255, default: 'main' })
|
@Column('varchar', { length: 255, default: 'main' })
|
||||||
prodBranch!: string;
|
prodBranch!: string;
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
|
||||||
applicationDeploymentRequestId!: string | null;
|
|
||||||
|
|
||||||
@Column('simple-json', { nullable: true })
|
|
||||||
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null;
|
|
||||||
|
|
||||||
@Column('text', { default: '' })
|
@Column('text', { default: '' })
|
||||||
description!: string;
|
description!: string;
|
||||||
|
|
||||||
@@ -91,7 +76,7 @@ export class Project {
|
|||||||
@OneToMany(() => Deployment, (deployment) => deployment.project)
|
@OneToMany(() => Deployment, (deployment) => deployment.project)
|
||||||
deployments!: Deployment[];
|
deployments!: Deployment[];
|
||||||
|
|
||||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||||
cascade: ['soft-remove']
|
cascade: ['soft-remove']
|
||||||
})
|
})
|
||||||
projectMembers!: ProjectMember[];
|
projectMembers!: ProjectMember[];
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { User } from './User';
|
|||||||
|
|
||||||
export enum Permission {
|
export enum Permission {
|
||||||
View = 'View',
|
View = 'View',
|
||||||
Edit = 'Edit'
|
Edit = 'Edit',
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
|||||||
@@ -12,10 +12,15 @@ import { UserOrganization } from './UserOrganization';
|
|||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Unique(['email'])
|
@Unique(['email'])
|
||||||
|
@Unique(['ethAddress'])
|
||||||
export class User {
|
export class User {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
|
// TODO: Set ethAddress as ID
|
||||||
|
@Column()
|
||||||
|
ethAddress!: string;
|
||||||
|
|
||||||
@Column('varchar', { length: 255, nullable: true })
|
@Column('varchar', { length: 255, nullable: true })
|
||||||
name!: string | null;
|
name!: string | null;
|
||||||
|
|
||||||
@@ -34,13 +39,17 @@ export class User {
|
|||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project, {
|
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||||
cascade: ['soft-remove']
|
cascade: ['soft-remove']
|
||||||
})
|
})
|
||||||
projectMembers!: ProjectMember[];
|
projectMembers!: ProjectMember[];
|
||||||
|
|
||||||
@OneToMany(() => UserOrganization, UserOrganization => UserOrganization.member, {
|
@OneToMany(
|
||||||
cascade: ['soft-remove']
|
() => UserOrganization,
|
||||||
})
|
(UserOrganization) => UserOrganization.member,
|
||||||
|
{
|
||||||
|
cascade: ['soft-remove']
|
||||||
|
}
|
||||||
|
)
|
||||||
userOrganizations!: UserOrganization[];
|
userOrganizations!: UserOrganization[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
import { Organization } from './Organization';
|
import { Organization } from './Organization';
|
||||||
|
|
||||||
enum Role {
|
export enum Role {
|
||||||
Owner = 'Owner',
|
Owner = 'Owner',
|
||||||
Maintainer = 'Maintainer',
|
Maintainer = 'Maintainer',
|
||||||
Reader = 'Reader',
|
Reader = 'Reader',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const OAUTH_CLIENT_TYPE = 'oauth-app';
|
|||||||
|
|
||||||
export const main = async (): Promise<void> => {
|
export const main = async (): Promise<void> => {
|
||||||
// TODO: get config path using cli
|
// TODO: get config path using cli
|
||||||
const { server, database, gitHub, registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { server, database, gitHub, registryConfig, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||||
|
|
||||||
const app = new OAuthApp({
|
const app = new OAuthApp({
|
||||||
clientType: OAUTH_CLIENT_TYPE,
|
clientType: OAUTH_CLIENT_TYPE,
|
||||||
@@ -27,13 +27,20 @@ export const main = async (): Promise<void> => {
|
|||||||
clientSecret: gitHub.oAuth.clientSecret
|
clientSecret: gitHub.oAuth.clientSecret
|
||||||
});
|
});
|
||||||
|
|
||||||
const db = new Database(database);
|
const db = new Database(database, misc);
|
||||||
await db.init();
|
await db.init();
|
||||||
|
|
||||||
const registry = new Registry(registryConfig);
|
const registry = new Registry(registryConfig);
|
||||||
const service = new Service({ gitHubConfig: gitHub, registryConfig }, db, app, registry);
|
const service = new Service(
|
||||||
|
{ gitHubConfig: gitHub, registryConfig },
|
||||||
|
db,
|
||||||
|
app,
|
||||||
|
registry
|
||||||
|
);
|
||||||
|
|
||||||
const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString();
|
const typeDefs = fs
|
||||||
|
.readFileSync(path.join(__dirname, 'schema.gql'))
|
||||||
|
.toString();
|
||||||
const resolvers = await createResolvers(service);
|
const resolvers = await createResolvers(service);
|
||||||
|
|
||||||
await createAndStartServer(server, typeDefs, resolvers, service);
|
await createAndStartServer(server, typeDefs, resolvers, service);
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import { DateTime } from 'luxon';
|
|||||||
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
||||||
|
|
||||||
import { RegistryConfig } from './config';
|
import { RegistryConfig } from './config';
|
||||||
import { ApplicationDeploymentRequest } from './entity/Project';
|
import {
|
||||||
import { ApplicationRecord, Deployment } from './entity/Deployment';
|
ApplicationRecord,
|
||||||
|
Deployment,
|
||||||
|
ApplicationDeploymentRequest
|
||||||
|
} from './entity/Deployment';
|
||||||
import { AppDeploymentRecord, PackageJSON } from './types';
|
import { AppDeploymentRecord, PackageJSON } from './types';
|
||||||
|
|
||||||
const log = debug('snowball:registry');
|
const log = debug('snowball:registry');
|
||||||
@@ -21,35 +24,54 @@ 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;
|
||||||
this.registry = new LaconicRegistry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
this.registry = new LaconicRegistry(
|
||||||
|
registryConfig.gqlEndpoint,
|
||||||
|
registryConfig.restEndpoint,
|
||||||
|
registryConfig.chainId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationRecord ({
|
async createApplicationRecord ({
|
||||||
|
appName,
|
||||||
packageJSON,
|
packageJSON,
|
||||||
commitHash,
|
commitHash,
|
||||||
appType,
|
appType,
|
||||||
repoUrl
|
repoUrl
|
||||||
}: {
|
}: {
|
||||||
packageJSON: PackageJSON
|
appName: string;
|
||||||
commitHash: string,
|
packageJSON: PackageJSON;
|
||||||
appType: string,
|
commitHash: string;
|
||||||
repoUrl: string
|
appType: string;
|
||||||
}): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> {
|
repoUrl: string;
|
||||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
}): Promise<{
|
||||||
|
applicationRecordId: string;
|
||||||
|
applicationRecordData: ApplicationRecord;
|
||||||
|
}> {
|
||||||
// Use laconic-sdk to publish record
|
// Use laconic-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(
|
||||||
type: APP_RECORD_TYPE,
|
{
|
||||||
name: packageJSON.name
|
type: APP_RECORD_TYPE,
|
||||||
}, true);
|
name: packageJSON.name
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Get next version of record
|
// Get next version of record
|
||||||
const bondRecords = records.filter((record: any) => record.bondId === this.registryConfig.bondId);
|
const bondRecords = records.filter(
|
||||||
const [latestBondRecord] = bondRecords.sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime());
|
(record: any) => record.bondId === this.registryConfig.bondId
|
||||||
const nextVersion = semverInc(latestBondRecord?.attributes.version ?? '0.0.0', 'patch');
|
);
|
||||||
|
const [latestBondRecord] = bondRecords.sort(
|
||||||
|
(a: any, b: any) =>
|
||||||
|
new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
|
||||||
|
);
|
||||||
|
const nextVersion = semverInc(
|
||||||
|
latestBondRecord?.attributes.version ?? '0.0.0',
|
||||||
|
'patch'
|
||||||
|
);
|
||||||
|
|
||||||
assert(nextVersion, 'Application record version not valid');
|
assert(nextVersion, 'Application record version not valid');
|
||||||
|
|
||||||
@@ -60,11 +82,16 @@ export class Registry {
|
|||||||
repository_ref: commitHash,
|
repository_ref: commitHash,
|
||||||
repository: [repoUrl],
|
repository: [repoUrl],
|
||||||
app_type: appType,
|
app_type: appType,
|
||||||
name: packageJSON.name,
|
name: appName,
|
||||||
...(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 }),
|
||||||
...(packageJSON.author && { author: typeof packageJSON.author === 'object' ? JSON.stringify(packageJSON.author) : packageJSON.author }),
|
...(packageJSON.author && {
|
||||||
|
author:
|
||||||
|
typeof packageJSON.author === 'object'
|
||||||
|
? JSON.stringify(packageJSON.author)
|
||||||
|
: packageJSON.author
|
||||||
|
}),
|
||||||
...(packageJSON.version && { app_version: packageJSON.version })
|
...(packageJSON.version && { app_version: packageJSON.version })
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,24 +108,43 @@ export class Registry {
|
|||||||
log('Application record data:', applicationRecord);
|
log('Application record data:', applicationRecord);
|
||||||
|
|
||||||
// TODO: Discuss computation of CRN
|
// TODO: Discuss computation of CRN
|
||||||
const crn = this.getCrn(packageJSON.name);
|
const crn = this.getCrn(appName);
|
||||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
||||||
|
|
||||||
await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee);
|
await this.registry.setName(
|
||||||
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee);
|
{ cid: result.data.id, crn },
|
||||||
await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee);
|
this.registryConfig.privateKey,
|
||||||
|
this.registryConfig.fee
|
||||||
|
);
|
||||||
|
await this.registry.setName(
|
||||||
|
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
this.registryConfig.fee
|
||||||
|
);
|
||||||
|
await this.registry.setName(
|
||||||
|
{
|
||||||
|
cid: result.data.id,
|
||||||
|
crn: `${crn}@${applicationRecord.repository_ref}`
|
||||||
|
},
|
||||||
|
this.registryConfig.privateKey,
|
||||||
|
this.registryConfig.fee
|
||||||
|
);
|
||||||
|
|
||||||
return { applicationRecordId: result.data.id, applicationRecordData: applicationRecord };
|
return {
|
||||||
|
applicationRecordId: result.data.id,
|
||||||
|
applicationRecordData: applicationRecord
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationDeploymentRequest (data: {
|
async createApplicationDeploymentRequest (data: {
|
||||||
|
deployment: Deployment,
|
||||||
appName: string,
|
appName: string,
|
||||||
commitHash: string,
|
packageJsonName: string,
|
||||||
repository: string,
|
repository: string,
|
||||||
environmentVariables: { [key: string]: string }
|
environmentVariables: { [key: string]: string }
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
applicationDeploymentRequestId: string,
|
applicationDeploymentRequestId: string;
|
||||||
applicationDeploymentRequestData: ApplicationDeploymentRequest
|
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||||
}> {
|
}> {
|
||||||
const crn = this.getCrn(data.appName);
|
const crn = this.getCrn(data.appName);
|
||||||
const records = await this.registry.resolveNames([crn]);
|
const records = await this.registry.resolveNames([crn]);
|
||||||
@@ -114,9 +160,9 @@ export class Registry {
|
|||||||
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: `${crn}@${applicationRecord.attributes.app_version}`,
|
||||||
|
dns: `${data.deployment.project.name}-${data.deployment.id}`,
|
||||||
|
|
||||||
// TODO: Not set in test-progressive-web-app CI
|
// TODO: Not set in test-progressive-web-app CI
|
||||||
// dns: '$CERC_REGISTRY_DEPLOYMENT_SHORT_HOSTNAME',
|
|
||||||
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
// 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
|
||||||
@@ -124,9 +170,11 @@ export class Registry {
|
|||||||
env: data.environmentVariables
|
env: data.environmentVariables
|
||||||
}),
|
}),
|
||||||
meta: JSON.stringify({
|
meta: JSON.stringify({
|
||||||
note: `Added by Snowball @ ${DateTime.utc().toFormat('EEE LLL dd HH:mm:ss \'UTC\' yyyy')}`,
|
note: `Added by Snowball @ ${DateTime.utc().toFormat(
|
||||||
|
"EEE LLL dd HH:mm:ss 'UTC' yyyy"
|
||||||
|
)}`,
|
||||||
repository: data.repository,
|
repository: data.repository,
|
||||||
repository_ref: data.commitHash
|
repository_ref: data.deployment.commitHash
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,31 +190,38 @@ export class Registry {
|
|||||||
log(`Application deployment request record published: ${result.data.id}`);
|
log(`Application deployment request record published: ${result.data.id}`);
|
||||||
log('Application deployment request data:', applicationDeploymentRequest);
|
log('Application deployment request data:', applicationDeploymentRequest);
|
||||||
|
|
||||||
return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest };
|
return {
|
||||||
|
applicationDeploymentRequestId: result.data.id,
|
||||||
|
applicationDeploymentRequestData: applicationDeploymentRequest
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch ApplicationDeploymentRecords for deployments
|
* Fetch ApplicationDeploymentRecords for deployments
|
||||||
*/
|
*/
|
||||||
async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> {
|
async getDeploymentRecords (
|
||||||
|
deployments: Deployment[]
|
||||||
|
): Promise<AppDeploymentRecord[]> {
|
||||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
||||||
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
|
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
|
||||||
const records = await this.registry.queryRecords({
|
const records = await this.registry.queryRecords(
|
||||||
type: APP_DEPLOYMENT_RECORD_TYPE
|
{
|
||||||
}, true);
|
type: APP_DEPLOYMENT_RECORD_TYPE
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Filter records with ApplicationRecord ids
|
// Filter records with ApplicationRecord ids
|
||||||
return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application));
|
return records.filter((record: AppDeploymentRecord) =>
|
||||||
|
deployments.some(
|
||||||
|
(deployment) =>
|
||||||
|
deployment.applicationRecordId === record.attributes.application
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCrn (packageJsonName: string): string {
|
getCrn (appName: string): string {
|
||||||
const [arg1, arg2] = packageJsonName.split('/');
|
assert(this.registryConfig.authority, "Authority doesn't exist");
|
||||||
|
return `crn://${this.registryConfig.authority}/applications/${appName}`;
|
||||||
if (arg2) {
|
|
||||||
const authority = arg1.replace('@', '');
|
|
||||||
return `crn://${authority}/applications/${arg2}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `crn://${arg1}/applications/${arg1}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
Query: {
|
Query: {
|
||||||
// TODO: add custom type for context
|
// TODO: add custom type for context
|
||||||
user: (_: any, __: any, context: any) => {
|
user: (_: any, __: any, context: any) => {
|
||||||
return service.getUser(context.userId);
|
return context.user;
|
||||||
},
|
},
|
||||||
|
|
||||||
organizations: async (_:any, __: any, context: any) => {
|
organizations: async (_:any, __: any, context: any) => {
|
||||||
return service.getOrganizationsByUserId(context.userId);
|
return service.getOrganizationsByUserId(context.user);
|
||||||
},
|
},
|
||||||
|
|
||||||
project: async (_: any, { projectId }: { projectId: string }) => {
|
project: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
@@ -26,14 +26,17 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
|
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
|
||||||
return service.getProjectsInOrganization(context.userId, organizationSlug);
|
return service.getProjectsInOrganization(context.user, organizationSlug);
|
||||||
},
|
},
|
||||||
|
|
||||||
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
return service.getDeploymentsByProjectId(projectId);
|
return service.getDeploymentsByProjectId(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
environmentVariables: async (_: any, { projectId }: { projectId: string }) => {
|
environmentVariables: async (
|
||||||
|
_: any,
|
||||||
|
{ projectId }: { projectId: string }
|
||||||
|
) => {
|
||||||
return service.getEnvironmentVariablesByProjectId(projectId);
|
return service.getEnvironmentVariablesByProjectId(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -42,31 +45,47 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
|
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
|
||||||
return service.searchProjects(context.userId, searchText);
|
return service.searchProjects(context.user, searchText);
|
||||||
},
|
},
|
||||||
|
|
||||||
domains: async (_:any, { projectId, filter }: { projectId: string, filter?: FindOptionsWhere<Domain> }) => {
|
domains: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
filter
|
||||||
|
}: { projectId: string; filter?: FindOptionsWhere<Domain> }
|
||||||
|
) => {
|
||||||
return service.getDomainsByProjectId(projectId, filter);
|
return service.getDomainsByProjectId(projectId, filter);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Return error in GQL response
|
// TODO: Return error in GQL response
|
||||||
Mutation: {
|
Mutation: {
|
||||||
removeProjectMember: async (_: any, { projectMemberId }: { projectMemberId: string }, context: any) => {
|
removeProjectMember: async (
|
||||||
|
_: any,
|
||||||
|
{ projectMemberId }: { projectMemberId: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.removeProjectMember(context.userId, projectMemberId);
|
return await service.removeProjectMember(context.user, projectMemberId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProjectMember: async (_: any, { projectMemberId, data }: {
|
updateProjectMember: async (
|
||||||
projectMemberId: string,
|
_: any,
|
||||||
data: {
|
{
|
||||||
permissions: Permission[]
|
projectMemberId,
|
||||||
|
data
|
||||||
|
}: {
|
||||||
|
projectMemberId: string;
|
||||||
|
data: {
|
||||||
|
permissions: Permission[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateProjectMember(projectMemberId, data);
|
return await service.updateProjectMember(projectMemberId, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -75,13 +94,19 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addProjectMember: async (_: any, { projectId, data }: {
|
addProjectMember: async (
|
||||||
projectId: string,
|
_: any,
|
||||||
data: {
|
{
|
||||||
email: string,
|
projectId,
|
||||||
permissions: Permission[]
|
data
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
data: {
|
||||||
|
email: string;
|
||||||
|
permissions: Permission[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.addProjectMember(projectId, data));
|
return Boolean(await service.addProjectMember(projectId, data));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -90,25 +115,51 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addEnvironmentVariables: async (_: any, { projectId, data }: { projectId: string, data: { environments: string[], key: string, value: string}[] }) => {
|
addEnvironmentVariables: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
data
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
data: { environments: string[]; key: string; value: string }[];
|
||||||
|
}
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.addEnvironmentVariables(projectId, data));
|
return Boolean(
|
||||||
|
await service.addEnvironmentVariables(projectId, data)
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateEnvironmentVariable: async (_: any, { environmentVariableId, data }: { environmentVariableId: string, data : DeepPartial<EnvironmentVariable>}) => {
|
updateEnvironmentVariable: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
environmentVariableId,
|
||||||
|
data
|
||||||
|
}: {
|
||||||
|
environmentVariableId: string;
|
||||||
|
data: DeepPartial<EnvironmentVariable>;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateEnvironmentVariable(environmentVariableId, data);
|
return await service.updateEnvironmentVariable(
|
||||||
|
environmentVariableId,
|
||||||
|
data
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeEnvironmentVariable: async (_: any, { environmentVariableId }: { environmentVariableId: string}) => {
|
removeEnvironmentVariable: async (
|
||||||
|
_: any,
|
||||||
|
{ environmentVariableId }: { environmentVariableId: string }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.removeEnvironmentVariable(environmentVariableId);
|
return await service.removeEnvironmentVariable(environmentVariableId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -117,25 +168,39 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
|
updateDeploymentToProd: async (
|
||||||
|
_: any,
|
||||||
|
{ deploymentId }: { deploymentId: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.updateDeploymentToProd(context.userId, deploymentId));
|
return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addProject: async (_: any, { organizationSlug, data }: { organizationSlug: string, data: DeepPartial<Project> }, context: any) => {
|
addProject: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
organizationSlug,
|
||||||
|
data
|
||||||
|
}: { organizationSlug: string; data: DeepPartial<Project> },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.addProject(context.userId, organizationSlug, data);
|
return await service.addProject(context.user, organizationSlug, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProject: async (_: any, { projectId, data }: { projectId: string, data: DeepPartial<Project> }) => {
|
updateProject: async (
|
||||||
|
_: any,
|
||||||
|
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateProject(projectId, data);
|
return await service.updateProject(projectId, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -144,9 +209,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }, context: any) => {
|
redeployToProd: async (
|
||||||
|
_: any,
|
||||||
|
{ deploymentId }: { deploymentId: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.redeployToProd(context.userId, deploymentId));
|
return Boolean(await service.redeployToProd(context.user, deploymentId));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -157,7 +226,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
try {
|
try {
|
||||||
return await service.deleteProject(projectId);
|
return await service.deleteProject(projectId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err); return false;
|
log(err);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -170,7 +240,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
rollbackDeployment: async (_: any, { projectId, deploymentId }: {deploymentId: string, projectId: string }) => {
|
rollbackDeployment: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
deploymentId
|
||||||
|
}: { deploymentId: string; projectId: string }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.rollbackDeployment(projectId, deploymentId);
|
return await service.rollbackDeployment(projectId, deploymentId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -179,7 +255,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addDomain: async (_: any, { projectId, data }: { projectId: string, data: { name: string } }) => {
|
addDomain: async (
|
||||||
|
_: any,
|
||||||
|
{ projectId, data }: { projectId: string; data: { name: string } }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return Boolean(await service.addDomain(projectId, data));
|
return Boolean(await service.addDomain(projectId, data));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -188,7 +267,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDomain: async (_: any, { domainId, data }: { domainId: string, data: DeepPartial<Domain>}) => {
|
updateDomain: async (
|
||||||
|
_: any,
|
||||||
|
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.updateDomain(domainId, data);
|
return await service.updateDomain(domainId, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -197,9 +279,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
authenticateGitHub: async (_: any, { code }: { code: string }, context: any) => {
|
authenticateGitHub: async (
|
||||||
|
_: any,
|
||||||
|
{ code }: { code: string },
|
||||||
|
context: any
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.authenticateGitHub(code, context.userId);
|
return await service.authenticateGitHub(code, context.user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -208,7 +294,7 @@ 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.userId, { gitHubToken: null });
|
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { SiweMessage, generateNonce } from 'siwe';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/nonce', async (_, res) => {
|
||||||
|
res.send(generateNonce());
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/validate', async (req, res) => {
|
||||||
|
const { message, signature } = req.body;
|
||||||
|
const { success, data } = await new SiweMessage(message).verify({
|
||||||
|
signature
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
req.session.address = data.address;
|
||||||
|
req.session.chainId = data.chainId;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({ success });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/session', (req, res) => {
|
||||||
|
if (req.session.address && req.session.chainId) {
|
||||||
|
res.send({ address: req.session.address, chainId: req.session.chainId });
|
||||||
|
} else {
|
||||||
|
res.status(401).send({ error: 'Unauthorized: No active session' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/logout', (req, res) => {
|
||||||
|
req.session.destroy((err) => {
|
||||||
|
if (err) {
|
||||||
|
return res.send({ success: false });
|
||||||
|
}
|
||||||
|
res.send({ success: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -188,10 +188,19 @@ type Query {
|
|||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
||||||
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
|
updateProjectMember(
|
||||||
|
projectMemberId: String!
|
||||||
|
data: UpdateProjectMemberInput
|
||||||
|
): Boolean!
|
||||||
removeProjectMember(projectMemberId: String!): Boolean!
|
removeProjectMember(projectMemberId: String!): Boolean!
|
||||||
addEnvironmentVariables(projectId: String!, data: [AddEnvironmentVariableInput!]): Boolean!
|
addEnvironmentVariables(
|
||||||
updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean!
|
projectId: String!
|
||||||
|
data: [AddEnvironmentVariableInput!]
|
||||||
|
): Boolean!
|
||||||
|
updateEnvironmentVariable(
|
||||||
|
environmentVariableId: String!
|
||||||
|
data: UpdateEnvironmentVariableInput!
|
||||||
|
): Boolean!
|
||||||
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
||||||
updateDeploymentToProd(deploymentId: String!): Boolean!
|
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||||
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
||||||
|
|||||||
@@ -1,22 +1,33 @@
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
import { ApolloServer } from 'apollo-server-express';
|
import { ApolloServer } from 'apollo-server-express';
|
||||||
import { createServer } from 'http';
|
import { createServer } from 'http';
|
||||||
import {
|
import {
|
||||||
ApolloServerPluginDrainHttpServer,
|
ApolloServerPluginDrainHttpServer,
|
||||||
ApolloServerPluginLandingPageLocalDefault
|
ApolloServerPluginLandingPageLocalDefault,
|
||||||
|
AuthenticationError
|
||||||
} from 'apollo-server-core';
|
} from 'apollo-server-core';
|
||||||
|
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';
|
||||||
|
|
||||||
import { ServerConfig } from './config';
|
import { ServerConfig } from './config';
|
||||||
import { DEFAULT_GQL_PATH, USER_ID } from './constants';
|
import { DEFAULT_GQL_PATH } from './constants';
|
||||||
import githubRouter from './routes/github';
|
import githubRouter from './routes/github';
|
||||||
|
import authRouter from './routes/auth';
|
||||||
import { Service } from './service';
|
import { Service } from './service';
|
||||||
|
|
||||||
const log = debug('snowball:server');
|
const log = debug('snowball:server');
|
||||||
|
|
||||||
|
declare module 'express-session' {
|
||||||
|
interface SessionData {
|
||||||
|
address: string;
|
||||||
|
chainId: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const createAndStartServer = async (
|
export const createAndStartServer = async (
|
||||||
serverConfig: ServerConfig,
|
serverConfig: ServerConfig,
|
||||||
typeDefs: TypeSource,
|
typeDefs: TypeSource,
|
||||||
@@ -24,6 +35,7 @@ export const createAndStartServer = async (
|
|||||||
service: Service
|
service: Service
|
||||||
): Promise<ApolloServer> => {
|
): Promise<ApolloServer> => {
|
||||||
const { host, port, gqlPath = DEFAULT_GQL_PATH } = serverConfig;
|
const { host, port, gqlPath = DEFAULT_GQL_PATH } = serverConfig;
|
||||||
|
const { appOriginUrl, secret, domain, trustProxy } = serverConfig.session;
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -39,9 +51,19 @@ export const createAndStartServer = async (
|
|||||||
const server = new ApolloServer({
|
const server = new ApolloServer({
|
||||||
schema,
|
schema,
|
||||||
csrfPrevention: true,
|
csrfPrevention: true,
|
||||||
context: () => {
|
context: async ({ req }) => {
|
||||||
// TODO: Use userId derived from auth token
|
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
||||||
return { userId: USER_ID };
|
|
||||||
|
const { address } = req.session;
|
||||||
|
|
||||||
|
if (!address) {
|
||||||
|
throw new AuthenticationError('Unauthorized: No active session');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find/create user from ETH address in request session
|
||||||
|
const user = await service.loadOrCreateUser(address);
|
||||||
|
|
||||||
|
return { user };
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// Proper shutdown for the HTTP server
|
// Proper shutdown for the HTTP server
|
||||||
@@ -52,13 +74,49 @@ export const createAndStartServer = async (
|
|||||||
|
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|
||||||
|
app.use(cors({
|
||||||
|
origin: appOriginUrl,
|
||||||
|
credentials: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
const sessionOptions: session.SessionOptions = {
|
||||||
|
secret: secret,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: true,
|
||||||
|
cookie: {
|
||||||
|
secure: new URL(appOriginUrl).protocol === 'https:',
|
||||||
|
// TODO: Set cookie maxAge and handle cookie expiry in frontend
|
||||||
|
// maxAge: SESSION_COOKIE_MAX_AGE,
|
||||||
|
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (domain) {
|
||||||
|
sessionOptions.cookie!.domain = domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trustProxy) {
|
||||||
|
// trust first proxy
|
||||||
|
app.set('trust proxy', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
session(sessionOptions)
|
||||||
|
);
|
||||||
|
|
||||||
server.applyMiddleware({
|
server.applyMiddleware({
|
||||||
app,
|
app,
|
||||||
path: gqlPath
|
path: gqlPath,
|
||||||
|
cors: {
|
||||||
|
origin: [appOriginUrl],
|
||||||
|
credentials: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.set('service', service);
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.set('service', service);
|
||||||
|
app.use('/auth', authRouter);
|
||||||
app.use('/api/github', githubRouter);
|
app.use('/api/github', githubRouter);
|
||||||
|
|
||||||
httpServer.listen(port, host, () => {
|
httpServer.listen(port, host, () => {
|
||||||
|
|||||||
@@ -15,15 +15,16 @@ import { Permission, ProjectMember } from './entity/ProjectMember';
|
|||||||
import { User } from './entity/User';
|
import { User } from './entity/User';
|
||||||
import { Registry } from './registry';
|
import { Registry } from './registry';
|
||||||
import { GitHubConfig, RegistryConfig } from './config';
|
import { GitHubConfig, RegistryConfig } from './config';
|
||||||
import { AppDeploymentRecord, GitPushEventPayload } from './types';
|
import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types';
|
||||||
|
import { Role } from './entity/UserOrganization';
|
||||||
|
|
||||||
const log = debug('snowball:service');
|
const log = debug('snowball:service');
|
||||||
|
|
||||||
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
|
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
gitHubConfig: GitHubConfig
|
gitHubConfig: GitHubConfig;
|
||||||
registryConfig: RegistryConfig
|
registryConfig: RegistryConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service {
|
export class Service {
|
||||||
@@ -71,7 +72,9 @@ export class Service {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (deployments.length) {
|
if (deployments.length) {
|
||||||
log(`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`);
|
log(
|
||||||
|
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch ApplicationDeploymentRecord for deployments
|
// Fetch ApplicationDeploymentRecord for deployments
|
||||||
const records = await this.registry.getDeploymentRecords(deployments);
|
const records = await this.registry.getDeploymentRecords(deployments);
|
||||||
@@ -91,10 +94,12 @@ export class Service {
|
|||||||
/**
|
/**
|
||||||
* Update deployments with ApplicationDeploymentRecord data
|
* Update deployments with ApplicationDeploymentRecord data
|
||||||
*/
|
*/
|
||||||
async updateDeploymentsWithRecordData (records: AppDeploymentRecord[]): Promise<void> {
|
async updateDeploymentsWithRecordData (
|
||||||
|
records: AppDeploymentRecord[]
|
||||||
|
): Promise<void> {
|
||||||
// Get deployments for ApplicationDeploymentRecords
|
// Get deployments for ApplicationDeploymentRecords
|
||||||
const deployments = await this.db.getDeployments({
|
const deployments = await this.db.getDeployments({
|
||||||
where: records.map(record => ({
|
where: records.map((record) => ({
|
||||||
applicationRecordId: record.attributes.application
|
applicationRecordId: record.attributes.application
|
||||||
})),
|
})),
|
||||||
order: {
|
order: {
|
||||||
@@ -103,38 +108,46 @@ export class Service {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get project IDs of deployments that are in production environment
|
// Get project IDs of deployments that are in production environment
|
||||||
const productionDeploymentProjectIds = deployments.reduce((acc, deployment): Set<string> => {
|
const productionDeploymentProjectIds = deployments.reduce(
|
||||||
if (deployment.environment === Environment.Production) {
|
(acc, deployment): Set<string> => {
|
||||||
acc.add(deployment.projectId);
|
if (deployment.environment === Environment.Production) {
|
||||||
}
|
acc.add(deployment.projectId);
|
||||||
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, new Set<string>());
|
},
|
||||||
|
new Set<string>()
|
||||||
|
);
|
||||||
|
|
||||||
// Set old deployments isCurrent to false
|
// Set old deployments isCurrent to false
|
||||||
await this.db.updateDeploymentsByProjectIds(Array.from(productionDeploymentProjectIds), { isCurrent: false });
|
await this.db.updateDeploymentsByProjectIds(
|
||||||
|
Array.from(productionDeploymentProjectIds),
|
||||||
|
{ isCurrent: false }
|
||||||
|
);
|
||||||
|
|
||||||
const recordToDeploymentsMap = deployments.reduce((acc: {[key: string]: Deployment}, deployment) => {
|
const recordToDeploymentsMap = deployments.reduce(
|
||||||
acc[deployment.applicationRecordId] = deployment;
|
(acc: { [key: string]: Deployment }, deployment) => {
|
||||||
return acc;
|
acc[deployment.applicationRecordId] = deployment;
|
||||||
}, {});
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
// Update deployment data for ApplicationDeploymentRecords
|
// Update deployment data for ApplicationDeploymentRecords
|
||||||
const deploymentUpdatePromises = records.map(async (record) => {
|
const deploymentUpdatePromises = records.map(async (record) => {
|
||||||
const deployment = recordToDeploymentsMap[record.attributes.application];
|
const deployment = recordToDeploymentsMap[record.attributes.application];
|
||||||
|
|
||||||
await this.db.updateDeploymentById(
|
await this.db.updateDeploymentById(deployment.id, {
|
||||||
deployment.id,
|
applicationDeploymentRecordId: record.id,
|
||||||
{
|
applicationDeploymentRecordData: record.attributes,
|
||||||
applicationDeploymentRecordId: record.id,
|
url: record.attributes.url,
|
||||||
applicationDeploymentRecordData: record.attributes,
|
status: DeploymentStatus.Ready,
|
||||||
url: record.attributes.url,
|
isCurrent: deployment.environment === Environment.Production
|
||||||
status: DeploymentStatus.Ready,
|
});
|
||||||
isCurrent: deployment.environment === Environment.Production
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
log(`Updated deployment ${deployment.id} with URL ${record.attributes.url}`);
|
log(
|
||||||
|
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(deploymentUpdatePromises);
|
await Promise.all(deploymentUpdatePromises);
|
||||||
@@ -148,15 +161,48 @@ export class Service {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadOrCreateUser (ethAddress: string): Promise<User> {
|
||||||
|
// Get user by ETH address
|
||||||
|
let user = await this.db.getUser({
|
||||||
|
where: {
|
||||||
|
ethAddress
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
const [org] = await this.db.getOrganizations({});
|
||||||
|
assert(org, 'No organizations exists in database');
|
||||||
|
|
||||||
|
// Create user with new address
|
||||||
|
user = await this.db.addUser({
|
||||||
|
email: `${ethAddress}@example.com`,
|
||||||
|
name: ethAddress,
|
||||||
|
isVerified: true,
|
||||||
|
ethAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.db.addUserOrganization({
|
||||||
|
member: user,
|
||||||
|
organization: org,
|
||||||
|
role: Role.Owner
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
async getOctokit (userId: string): Promise<Octokit> {
|
async getOctokit (userId: string): Promise<Octokit> {
|
||||||
const user = await this.db.getUser({ where: { id: userId } });
|
const user = await this.db.getUser({ where: { id: userId } });
|
||||||
assert(user && user.gitHubToken, 'User needs to be authenticated with GitHub token');
|
assert(
|
||||||
|
user && user.gitHubToken,
|
||||||
|
'User needs to be authenticated with GitHub token'
|
||||||
|
);
|
||||||
|
|
||||||
return new Octokit({ auth: user.gitHubToken });
|
return new Octokit({ auth: user.gitHubToken });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
|
async getOrganizationsByUserId (user: User): Promise<Organization[]> {
|
||||||
const dbOrganizations = await this.db.getOrganizationsByUserId(userId);
|
const dbOrganizations = await this.db.getOrganizationsByUserId(user.id);
|
||||||
return dbOrganizations;
|
return dbOrganizations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +211,8 @@ export class Service {
|
|||||||
return dbProject;
|
return dbProject;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsInOrganization (userId:string, organizationSlug: string): Promise<Project[]> {
|
async getProjectsInOrganization (user: User, organizationSlug: string): Promise<Project[]> {
|
||||||
const dbProjects = await this.db.getProjectsInOrganization(userId, organizationSlug);
|
const dbProjects = await this.db.getProjectsInOrganization(user.id, organizationSlug);
|
||||||
return dbProjects;
|
return dbProjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,35 +221,49 @@ export class Service {
|
|||||||
return dbDeployments;
|
return dbDeployments;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEnvironmentVariablesByProjectId (projectId: string): Promise<EnvironmentVariable[]> {
|
async getEnvironmentVariablesByProjectId (
|
||||||
const dbEnvironmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId);
|
projectId: string
|
||||||
|
): Promise<EnvironmentVariable[]> {
|
||||||
|
const dbEnvironmentVariables =
|
||||||
|
await this.db.getEnvironmentVariablesByProjectId(projectId);
|
||||||
return dbEnvironmentVariables;
|
return dbEnvironmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMembersByProjectId (projectId: string): Promise<ProjectMember[]> {
|
async getProjectMembersByProjectId (
|
||||||
const dbProjectMembers = await this.db.getProjectMembersByProjectId(projectId);
|
projectId: string
|
||||||
|
): Promise<ProjectMember[]> {
|
||||||
|
const dbProjectMembers =
|
||||||
|
await this.db.getProjectMembersByProjectId(projectId);
|
||||||
return dbProjectMembers;
|
return dbProjectMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchProjects (userId: string, searchText: string): Promise<Project[]> {
|
async searchProjects (user: User, searchText: string): Promise<Project[]> {
|
||||||
const dbProjects = await this.db.getProjectsBySearchText(userId, searchText);
|
const dbProjects = await this.db.getProjectsBySearchText(user.id, searchText);
|
||||||
return dbProjects;
|
return dbProjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDomainsByProjectId (projectId: string, filter?: FindOptionsWhere<Domain>): Promise<Domain[]> {
|
async getDomainsByProjectId (
|
||||||
|
projectId: string,
|
||||||
|
filter?: FindOptionsWhere<Domain>
|
||||||
|
): Promise<Domain[]> {
|
||||||
const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
|
const dbDomains = await this.db.getDomainsByProjectId(projectId, filter);
|
||||||
return dbDomains;
|
return dbDomains;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectMember (projectMemberId: string, data: {permissions: Permission[]}): Promise<boolean> {
|
async updateProjectMember (
|
||||||
|
projectMemberId: string,
|
||||||
|
data: { permissions: Permission[] }
|
||||||
|
): Promise<boolean> {
|
||||||
return this.db.updateProjectMemberById(projectMemberId, data);
|
return this.db.updateProjectMemberById(projectMemberId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProjectMember (projectId: string,
|
async addProjectMember (
|
||||||
|
projectId: string,
|
||||||
data: {
|
data: {
|
||||||
email: string,
|
email: string;
|
||||||
permissions: Permission[]
|
permissions: Permission[];
|
||||||
}): Promise<ProjectMember> {
|
}
|
||||||
|
): Promise<ProjectMember> {
|
||||||
// TODO: Send invitation
|
// TODO: Send invitation
|
||||||
let user = await this.db.getUser({
|
let user = await this.db.getUser({
|
||||||
where: {
|
where: {
|
||||||
@@ -231,50 +291,62 @@ export class Service {
|
|||||||
return newProjectMember;
|
return newProjectMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeProjectMember (userId: string, projectMemberId: string): Promise<boolean> {
|
async removeProjectMember (user: User, projectMemberId: string): Promise<boolean> {
|
||||||
const member = await this.db.getProjectMemberById(projectMemberId);
|
const member = await this.db.getProjectMemberById(projectMemberId);
|
||||||
|
|
||||||
if (String(member.member.id) === userId) {
|
if (String(member.member.id) === user.id) {
|
||||||
throw new Error('Invalid operation: cannot remove self');
|
throw new Error('Invalid operation: cannot remove self');
|
||||||
}
|
}
|
||||||
|
|
||||||
const memberProject = member.project;
|
const memberProject = member.project;
|
||||||
assert(memberProject);
|
assert(memberProject);
|
||||||
|
|
||||||
if (String(userId) === String(memberProject.owner.id)) {
|
if (String(user.id) === String(memberProject.owner.id)) {
|
||||||
return this.db.removeProjectMemberById(projectMemberId);
|
return this.db.removeProjectMemberById(projectMemberId);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid operation: not authorized');
|
throw new Error('Invalid operation: not authorized');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addEnvironmentVariables (projectId: string, data: { environments: string[], key: string, value: string}[]): Promise<EnvironmentVariable[]> {
|
async addEnvironmentVariables (
|
||||||
const formattedEnvironmentVariables = data.map((environmentVariable) => {
|
projectId: string,
|
||||||
return environmentVariable.environments.map((environment) => {
|
data: { environments: string[]; key: string; value: string }[]
|
||||||
return ({
|
): Promise<EnvironmentVariable[]> {
|
||||||
key: environmentVariable.key,
|
const formattedEnvironmentVariables = data
|
||||||
value: environmentVariable.value,
|
.map((environmentVariable) => {
|
||||||
environment: environment as Environment,
|
return environmentVariable.environments.map((environment) => {
|
||||||
project: Object.assign(new Project(), {
|
return {
|
||||||
id: projectId
|
key: environmentVariable.key,
|
||||||
})
|
value: environmentVariable.value,
|
||||||
|
environment: environment as Environment,
|
||||||
|
project: Object.assign(new Project(), {
|
||||||
|
id: projectId
|
||||||
|
})
|
||||||
|
};
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
}).flat();
|
.flat();
|
||||||
|
|
||||||
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(formattedEnvironmentVariables);
|
const savedEnvironmentVariables = await this.db.addEnvironmentVariables(
|
||||||
|
formattedEnvironmentVariables
|
||||||
|
);
|
||||||
return savedEnvironmentVariables;
|
return savedEnvironmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEnvironmentVariable (environmentVariableId: string, data : DeepPartial<EnvironmentVariable>): Promise<boolean> {
|
async updateEnvironmentVariable (
|
||||||
|
environmentVariableId: string,
|
||||||
|
data: DeepPartial<EnvironmentVariable>
|
||||||
|
): Promise<boolean> {
|
||||||
return this.db.updateEnvironmentVariable(environmentVariableId, data);
|
return this.db.updateEnvironmentVariable(environmentVariableId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeEnvironmentVariable (environmentVariableId: string): Promise<boolean> {
|
async removeEnvironmentVariable (
|
||||||
|
environmentVariableId: string
|
||||||
|
): Promise<boolean> {
|
||||||
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentToProd (userId: string, deploymentId: string): Promise<Deployment> {
|
async updateDeploymentToProd (user: User, deploymentId: string): Promise<Deployment> {
|
||||||
const oldDeployment = await this.db.getDeployment({
|
const oldDeployment = await this.db.getDeployment({
|
||||||
where: { id: deploymentId },
|
where: { id: deploymentId },
|
||||||
relations: {
|
relations: {
|
||||||
@@ -286,11 +358,14 @@ export class Service {
|
|||||||
throw new Error('Deployment does not exist');
|
throw new Error('Deployment does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
const prodBranchDomains = await this.db.getDomainsByProjectId(oldDeployment.project.id, { branch: oldDeployment.project.prodBranch });
|
const prodBranchDomains = await this.db.getDomainsByProjectId(
|
||||||
|
oldDeployment.project.id,
|
||||||
|
{ branch: oldDeployment.project.prodBranch }
|
||||||
|
);
|
||||||
|
|
||||||
const octokit = await this.getOctokit(userId);
|
const octokit = await this.getOctokit(user.id);
|
||||||
|
|
||||||
const newDeployment = await this.createDeployment(userId,
|
const newDeployment = await this.createDeployment(user.id,
|
||||||
octokit,
|
octokit,
|
||||||
{
|
{
|
||||||
project: oldDeployment.project,
|
project: oldDeployment.project,
|
||||||
@@ -311,7 +386,9 @@ export class Service {
|
|||||||
recordData: { repoUrl?: string } = {}
|
recordData: { repoUrl?: string } = {}
|
||||||
): Promise<Deployment> {
|
): Promise<Deployment> {
|
||||||
assert(data.project?.repository, 'Project repository not found');
|
assert(data.project?.repository, 'Project repository not found');
|
||||||
log(`Creating deployment in project ${data.project.name} from branch ${data.branch}`);
|
log(
|
||||||
|
`Creating deployment in project ${data.project.name} from branch ${data.branch}`
|
||||||
|
);
|
||||||
const [owner, repo] = data.project.repository.split('/');
|
const [owner, repo] = data.project.repository.split('/');
|
||||||
|
|
||||||
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||||
@@ -326,28 +403,37 @@ export class Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
||||||
const packageJSON = JSON.parse(atob(packageJSONData.content));
|
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
|
||||||
|
|
||||||
|
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||||
|
|
||||||
if (!recordData.repoUrl) {
|
if (!recordData.repoUrl) {
|
||||||
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
const { data: repoDetails } = await octokit.rest.repos.get({
|
||||||
|
owner,
|
||||||
|
repo
|
||||||
|
});
|
||||||
recordData.repoUrl = repoDetails.html_url;
|
recordData.repoUrl = repoDetails.html_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
||||||
const { applicationRecordId, applicationRecordData } = await this.registry.createApplicationRecord({
|
const { applicationRecordId, applicationRecordData } =
|
||||||
packageJSON,
|
await this.registry.createApplicationRecord({
|
||||||
appType: data.project!.template!,
|
appName: repo,
|
||||||
commitHash: data.commitHash!,
|
packageJSON,
|
||||||
repoUrl: recordData.repoUrl
|
appType: data.project!.template!,
|
||||||
});
|
commitHash: data.commitHash!,
|
||||||
|
repoUrl: recordData.repoUrl
|
||||||
|
});
|
||||||
|
|
||||||
// Update previous deployment with prod branch domain
|
// Update previous deployment with prod branch domain
|
||||||
// TODO: Fix unique constraint error for domain
|
// TODO: Fix unique constraint error for domain
|
||||||
await this.db.updateDeployment({
|
if (data.domain) {
|
||||||
domainId: data.domain?.id
|
await this.db.updateDeployment({
|
||||||
}, {
|
domainId: data.domain.id
|
||||||
domain: null
|
}, {
|
||||||
});
|
domain: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const newDeployment = await this.db.addDeployment({
|
const newDeployment = await this.db.addDeployment({
|
||||||
project: data.project,
|
project: data.project,
|
||||||
@@ -365,10 +451,30 @@ export class Service {
|
|||||||
});
|
});
|
||||||
|
|
||||||
log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`);
|
log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`);
|
||||||
|
|
||||||
|
const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(data.project.id!, { environment: Environment.Production });
|
||||||
|
|
||||||
|
const environmentVariablesObj = environmentVariables.reduce((acc, env) => {
|
||||||
|
acc[env.key] = env.value;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {} as { [key: string]: string });
|
||||||
|
|
||||||
|
const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest(
|
||||||
|
{
|
||||||
|
deployment: newDeployment,
|
||||||
|
appName: repo,
|
||||||
|
packageJsonName: packageJSON.name,
|
||||||
|
repository: recordData.repoUrl,
|
||||||
|
environmentVariables: environmentVariablesObj
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData });
|
||||||
|
|
||||||
return newDeployment;
|
return newDeployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProject (userId: string, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> {
|
async addProject (user: User, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> {
|
||||||
const organization = await this.db.getOrganization({
|
const organization = await this.db.getOrganization({
|
||||||
where: {
|
where: {
|
||||||
slug: organizationSlug
|
slug: organizationSlug
|
||||||
@@ -378,12 +484,14 @@ export class Service {
|
|||||||
throw new Error('Organization does not exist');
|
throw new Error('Organization does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = await this.db.addProject(userId, organization.id, data);
|
const project = await this.db.addProject(user, organization.id, data);
|
||||||
|
|
||||||
const octokit = await this.getOctokit(userId);
|
const octokit = await this.getOctokit(user.id);
|
||||||
const [owner, repo] = project.repository.split('/');
|
const [owner, repo] = project.repository.split('/');
|
||||||
|
|
||||||
const { data: [latestCommit] } = await octokit.rest.repos.listCommits({
|
const {
|
||||||
|
data: [latestCommit]
|
||||||
|
} = await octokit.rest.repos.listCommits({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
sha: project.prodBranch,
|
sha: project.prodBranch,
|
||||||
@@ -393,7 +501,7 @@ export class Service {
|
|||||||
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo });
|
||||||
|
|
||||||
// Create deployment with prod branch and latest commit
|
// Create deployment with prod branch and latest commit
|
||||||
const newDeployment = await this.createDeployment(userId,
|
await this.createDeployment(user.id,
|
||||||
octokit,
|
octokit,
|
||||||
{
|
{
|
||||||
project,
|
project,
|
||||||
@@ -408,27 +516,6 @@ export class Service {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(project.id, { environment: Environment.Production });
|
|
||||||
|
|
||||||
const environmentVariablesObj = environmentVariables.reduce((acc, env) => {
|
|
||||||
acc[env.key] = env.value;
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {} as { [key: string]: string });
|
|
||||||
|
|
||||||
const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest(
|
|
||||||
{
|
|
||||||
appName: newDeployment.applicationRecordData.name!,
|
|
||||||
commitHash: latestCommit.sha,
|
|
||||||
repository: repoDetails.html_url,
|
|
||||||
environmentVariables: environmentVariablesObj
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.db.updateProjectById(project.id, {
|
|
||||||
applicationDeploymentRequestId,
|
|
||||||
applicationDeploymentRequestData
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.createRepoHook(octokit, project);
|
await this.createRepoHook(octokit, project);
|
||||||
|
|
||||||
return project;
|
return project;
|
||||||
@@ -441,7 +528,10 @@ export class Service {
|
|||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
config: {
|
config: {
|
||||||
url: new URL('api/github/webhook', this.config.gitHubConfig.webhookUrl).href,
|
url: new URL(
|
||||||
|
'api/github/webhook',
|
||||||
|
this.config.gitHubConfig.webhookUrl
|
||||||
|
).href,
|
||||||
content_type: 'json'
|
content_type: 'json'
|
||||||
},
|
},
|
||||||
events: ['push']
|
events: ['push']
|
||||||
@@ -449,9 +539,13 @@ export class Service {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
|
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#create-a-repository-webhook--status-codes
|
||||||
if (
|
if (
|
||||||
!(err instanceof RequestError &&
|
!(
|
||||||
err.status === 422 &&
|
err instanceof RequestError &&
|
||||||
(err.response?.data as any).errors.some((err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR))
|
err.status === 422 &&
|
||||||
|
(err.response?.data as any).errors.some(
|
||||||
|
(err: any) => err.message === GITHUB_UNIQUE_WEBHOOK_ERROR
|
||||||
|
)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
@@ -463,7 +557,9 @@ export class Service {
|
|||||||
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
|
async handleGitHubPush (data: GitPushEventPayload): Promise<void> {
|
||||||
const { repository, ref, head_commit: headCommit } = data;
|
const { repository, ref, head_commit: headCommit } = data;
|
||||||
log(`Handling GitHub push event from repository: ${repository.full_name}`);
|
log(`Handling GitHub push event from repository: ${repository.full_name}`);
|
||||||
const projects = await this.db.getProjects({ where: { repository: repository.full_name } });
|
const projects = await this.db.getProjects({
|
||||||
|
where: { repository: repository.full_name }
|
||||||
|
});
|
||||||
|
|
||||||
if (!projects.length) {
|
if (!projects.length) {
|
||||||
log(`No projects found for repository ${repository.full_name}`);
|
log(`No projects found for repository ${repository.full_name}`);
|
||||||
@@ -475,23 +571,29 @@ export class Service {
|
|||||||
|
|
||||||
for await (const project of projects) {
|
for await (const project of projects) {
|
||||||
const octokit = await this.getOctokit(project.ownerId);
|
const octokit = await this.getOctokit(project.ownerId);
|
||||||
const [domain] = await this.db.getDomainsByProjectId(project.id, { branch });
|
const [domain] = await this.db.getDomainsByProjectId(project.id, {
|
||||||
|
branch
|
||||||
|
});
|
||||||
|
|
||||||
// Create deployment with branch and latest commit in GitHub data
|
// Create deployment with branch and latest commit in GitHub data
|
||||||
await this.createDeployment(project.ownerId,
|
await this.createDeployment(project.ownerId, octokit, {
|
||||||
octokit,
|
project,
|
||||||
{
|
branch,
|
||||||
project,
|
environment:
|
||||||
branch,
|
project.prodBranch === branch
|
||||||
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview,
|
? Environment.Production
|
||||||
domain,
|
: Environment.Preview,
|
||||||
commitHash: headCommit.id,
|
domain,
|
||||||
commitMessage: headCommit.message
|
commitHash: headCommit.id,
|
||||||
});
|
commitMessage: headCommit.message
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
|
async updateProject (
|
||||||
|
projectId: string,
|
||||||
|
data: DeepPartial<Project>
|
||||||
|
): Promise<boolean> {
|
||||||
return this.db.updateProjectById(projectId, data);
|
return this.db.updateProjectById(projectId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,13 +610,15 @@ export class Service {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (domainsRedirectedFrom.length > 0) {
|
if (domainsRedirectedFrom.length > 0) {
|
||||||
throw new Error('Cannot delete domain since it has redirects from other domains');
|
throw new Error(
|
||||||
|
'Cannot delete domain since it has redirects from other domains'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.db.deleteDomainById(domainId);
|
return this.db.deleteDomainById(domainId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async redeployToProd (userId: string, deploymentId: string): Promise<Deployment> {
|
async redeployToProd (user: User, deploymentId: string): Promise<Deployment> {
|
||||||
const oldDeployment = await this.db.getDeployment({
|
const oldDeployment = await this.db.getDeployment({
|
||||||
relations: {
|
relations: {
|
||||||
project: true,
|
project: true,
|
||||||
@@ -530,9 +634,9 @@ export class Service {
|
|||||||
throw new Error('Deployment not found');
|
throw new Error('Deployment not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const octokit = await this.getOctokit(userId);
|
const octokit = await this.getOctokit(user.id);
|
||||||
|
|
||||||
const newDeployment = await this.createDeployment(userId,
|
const newDeployment = await this.createDeployment(user.id,
|
||||||
octokit,
|
octokit,
|
||||||
{
|
{
|
||||||
project: oldDeployment.project,
|
project: oldDeployment.project,
|
||||||
@@ -547,7 +651,10 @@ export class Service {
|
|||||||
return newDeployment;
|
return newDeployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> {
|
async rollbackDeployment (
|
||||||
|
projectId: string,
|
||||||
|
deploymentId: string
|
||||||
|
): Promise<boolean> {
|
||||||
// TODO: Implement transactions
|
// TODO: Implement transactions
|
||||||
const oldCurrentDeployment = await this.db.getDeployment({
|
const oldCurrentDeployment = await this.db.getDeployment({
|
||||||
relations: {
|
relations: {
|
||||||
@@ -565,16 +672,25 @@ export class Service {
|
|||||||
throw new Error('Current deployment doesnot exist');
|
throw new Error('Current deployment doesnot exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(oldCurrentDeployment.id, { isCurrent: false, domain: null });
|
const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(
|
||||||
|
oldCurrentDeployment.id,
|
||||||
|
{ isCurrent: false, domain: null }
|
||||||
|
);
|
||||||
|
|
||||||
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(deploymentId, { isCurrent: true, domain: oldCurrentDeployment?.domain });
|
const newCurrentDeploymentUpdate = await this.db.updateDeploymentById(
|
||||||
|
deploymentId,
|
||||||
|
{ isCurrent: true, domain: oldCurrentDeployment?.domain }
|
||||||
|
);
|
||||||
|
|
||||||
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
|
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDomain (projectId: string, data: { name: string }): Promise<{
|
async addDomain (
|
||||||
primaryDomain: Domain,
|
projectId: string,
|
||||||
redirectedDomain: Domain
|
data: { name: string }
|
||||||
|
): Promise<{
|
||||||
|
primaryDomain: Domain;
|
||||||
|
redirectedDomain: Domain;
|
||||||
}> {
|
}> {
|
||||||
const currentProject = await this.db.getProjectById(projectId);
|
const currentProject = await this.db.getProjectById(projectId);
|
||||||
|
|
||||||
@@ -599,12 +715,20 @@ export class Service {
|
|||||||
redirectTo: savedPrimaryDomain
|
redirectTo: savedPrimaryDomain
|
||||||
};
|
};
|
||||||
|
|
||||||
const savedRedirectedDomain = await this.db.addDomain(redirectedDomainDetails);
|
const savedRedirectedDomain = await this.db.addDomain(
|
||||||
|
redirectedDomainDetails
|
||||||
|
);
|
||||||
|
|
||||||
return { primaryDomain: savedPrimaryDomain, redirectedDomain: savedRedirectedDomain };
|
return {
|
||||||
|
primaryDomain: savedPrimaryDomain,
|
||||||
|
redirectedDomain: savedRedirectedDomain
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDomain (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
|
async updateDomain (
|
||||||
|
domainId: string,
|
||||||
|
data: DeepPartial<Domain>
|
||||||
|
): Promise<boolean> {
|
||||||
const domain = await this.db.getDomain({
|
const domain = await this.db.getDomain({
|
||||||
where: {
|
where: {
|
||||||
id: domainId
|
id: domainId
|
||||||
@@ -645,7 +769,9 @@ export class Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (redirectedDomain.redirectToId) {
|
if (redirectedDomain.redirectToId) {
|
||||||
throw new Error('Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.');
|
throw new Error(
|
||||||
|
'Unable to redirect to the domain because it is already redirecting elsewhere. Redirects cannot be chained.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
newDomain.redirectTo = redirectedDomain;
|
newDomain.redirectTo = redirectedDomain;
|
||||||
@@ -656,17 +782,17 @@ export class Service {
|
|||||||
return updateResult;
|
return updateResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticateGitHub (code:string, userId: string): Promise<{token: string}> {
|
async authenticateGitHub (code:string, user: User): Promise<{token: string}> {
|
||||||
const { authentication: { token } } = await this.oauthApp.createToken({
|
const { authentication: { token } } = await this.oauthApp.createToken({
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.db.updateUser(userId, { gitHubToken: token });
|
await this.db.updateUser(user, { gitHubToken: token });
|
||||||
|
|
||||||
return { token };
|
return { token };
|
||||||
}
|
}
|
||||||
|
|
||||||
async unauthenticateGitHub (userId: string, data: DeepPartial<User>): Promise<boolean> {
|
async unauthenticateGitHub (user: User, data: DeepPartial<User>): Promise<boolean> {
|
||||||
return this.db.updateUser(userId, data);
|
return this.db.updateUser(user, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export interface PackageJSON {
|
export interface PackageJSON {
|
||||||
name?: string;
|
name: string;
|
||||||
version?: string;
|
version: string;
|
||||||
author?: string;
|
author?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
homepage?: string;
|
homepage?: string;
|
||||||
@@ -47,5 +47,5 @@ interface RegistryRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AppDeploymentRecord extends RegistryRecord {
|
export interface AppDeploymentRecord extends RegistryRecord {
|
||||||
attributes: AppDeploymentRecordAttributes
|
attributes: AppDeploymentRecordAttributes;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import toml from 'toml';
|
import toml from 'toml';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||||
|
|
||||||
const log = debug('snowball:utils');
|
const log = debug('snowball:utils');
|
||||||
|
|
||||||
@@ -19,3 +20,49 @@ export const getConfig = async <ConfigType>(
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkFileExists = async (filePath: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
log(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEntities = async (filePath: string): Promise<any> => {
|
||||||
|
const entitiesData = await fs.readFile(filePath, 'utf-8');
|
||||||
|
const entities = JSON.parse(entitiesData);
|
||||||
|
return entities;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||||
|
entityType: EntityTarget<Entity>,
|
||||||
|
dataSource: DataSource,
|
||||||
|
entities: any,
|
||||||
|
relations?: any | undefined
|
||||||
|
): Promise<Entity[]> => {
|
||||||
|
const entityRepository = dataSource.getRepository(entityType);
|
||||||
|
|
||||||
|
const savedEntity: Entity[] = [];
|
||||||
|
|
||||||
|
for (const entityData of entities) {
|
||||||
|
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
|
||||||
|
|
||||||
|
if (relations) {
|
||||||
|
for (const field in relations) {
|
||||||
|
const valueIndex = String(field + 'Index');
|
||||||
|
|
||||||
|
entity = {
|
||||||
|
...entity,
|
||||||
|
[field]: relations[field][entityData[valueIndex]]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dbEntity = await entityRepository.save(entity);
|
||||||
|
savedEntity.push(dbEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return savedEntity;
|
||||||
|
};
|
||||||
|
|||||||
@@ -18,4 +18,4 @@ const main = async () => {
|
|||||||
deleteFile(config.database.dbPath);
|
deleteFile(config.database.dbPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
main().catch(err => log(err));
|
main().catch((err) => log(err));
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"domainIndex":0,
|
"domainIndex": 0,
|
||||||
"createdByIndex": 0,
|
"createdByIndex": 0,
|
||||||
"id":"ffhae3zq",
|
"id": "ffhae3zq",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": true,
|
"isCurrent": true,
|
||||||
"applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "xqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -16,14 +18,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"domainIndex":1,
|
"domainIndex": 1,
|
||||||
"createdByIndex": 0,
|
"createdByIndex": 0,
|
||||||
"id":"vehagei8",
|
"id": "vehagei8",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Preview",
|
"environment": "Preview",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "wqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -31,14 +35,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"domainIndex":2,
|
"domainIndex": 2,
|
||||||
"createdByIndex": 0,
|
"createdByIndex": 0,
|
||||||
"id":"qmgekyte",
|
"id": "qmgekyte",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Development",
|
"environment": "Development",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "kqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -48,12 +54,14 @@
|
|||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"domainIndex": null,
|
"domainIndex": null,
|
||||||
"createdByIndex": 0,
|
"createdByIndex": 0,
|
||||||
"id":"f8wsyim6",
|
"id": "f8wsyim6",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "yqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "prod",
|
"branch": "prod",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -61,14 +69,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 1,
|
"projectIndex": 1,
|
||||||
"domainIndex":3,
|
"domainIndex": 3,
|
||||||
"createdByIndex": 1,
|
"createdByIndex": 1,
|
||||||
"id":"eO8cckxk",
|
"id": "eO8cckxk",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": true,
|
"isCurrent": true,
|
||||||
"applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -76,14 +86,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 1,
|
"projectIndex": 1,
|
||||||
"domainIndex":4,
|
"domainIndex": 4,
|
||||||
"createdByIndex": 1,
|
"createdByIndex": 1,
|
||||||
"id":"yaq0t5yw",
|
"id": "yaq0t5yw",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Preview",
|
"environment": "Preview",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "tqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -91,14 +103,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 1,
|
"projectIndex": 1,
|
||||||
"domainIndex":5,
|
"domainIndex": 5,
|
||||||
"createdByIndex": 1,
|
"createdByIndex": 1,
|
||||||
"id":"hwwr6sbx",
|
"id": "hwwr6sbx",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Development",
|
"environment": "Development",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
|
"applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "eqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -106,14 +120,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"domainIndex":9,
|
"domainIndex": 9,
|
||||||
"createdByIndex": 2,
|
"createdByIndex": 2,
|
||||||
"id":"ndxje48a",
|
"id": "ndxje48a",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": true,
|
"isCurrent": true,
|
||||||
"applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "dqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -121,14 +137,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"domainIndex":7,
|
"domainIndex": 7,
|
||||||
"createdByIndex": 2,
|
"createdByIndex": 2,
|
||||||
"id":"gtgpgvei",
|
"id": "gtgpgvei",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Preview",
|
"environment": "Preview",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "aqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -136,14 +154,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"domainIndex":8,
|
"domainIndex": 8,
|
||||||
"createdByIndex": 2,
|
"createdByIndex": 2,
|
||||||
"id":"b4bpthjr",
|
"id": "b4bpthjr",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Development",
|
"environment": "Development",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "uqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -153,12 +173,14 @@
|
|||||||
"projectIndex": 3,
|
"projectIndex": 3,
|
||||||
"domainIndex": 6,
|
"domainIndex": 6,
|
||||||
"createdByIndex": 2,
|
"createdByIndex": 2,
|
||||||
"id":"b4bpthjr",
|
"id": "b4bpthjr",
|
||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": true,
|
"isCurrent": true,
|
||||||
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
||||||
"applicationRecordData": {},
|
"applicationRecordData": {},
|
||||||
|
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"applicationDeploymentRequestData": {},
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
|
|||||||
@@ -2,77 +2,55 @@
|
|||||||
{
|
{
|
||||||
"memberIndex": 1,
|
"memberIndex": 1,
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 2,
|
"memberIndex": 2,
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"permissions": [
|
"permissions": ["View", "Edit"],
|
||||||
"View",
|
|
||||||
"Edit"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 2,
|
"memberIndex": 2,
|
||||||
"projectIndex": 1,
|
"projectIndex": 1,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 0,
|
"memberIndex": 0,
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 1,
|
"memberIndex": 1,
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"permissions": [
|
"permissions": ["View", "Edit"],
|
||||||
"View",
|
|
||||||
"Edit"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 0,
|
"memberIndex": 0,
|
||||||
"projectIndex": 3,
|
"projectIndex": 3,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 2,
|
"memberIndex": 2,
|
||||||
"projectIndex": 3,
|
"projectIndex": 3,
|
||||||
"permissions": [
|
"permissions": ["View", "Edit"],
|
||||||
"View",
|
|
||||||
"Edit"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 1,
|
"memberIndex": 1,
|
||||||
"projectIndex": 4,
|
"projectIndex": 4,
|
||||||
"permissions": [
|
"permissions": ["View"],
|
||||||
"View"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"memberIndex": 2,
|
"memberIndex": 2,
|
||||||
"projectIndex": 4,
|
"projectIndex": 4,
|
||||||
"permissions": [
|
"permissions": ["View", "Edit"],
|
||||||
"View",
|
|
||||||
"Edit"
|
|
||||||
],
|
|
||||||
"isPending": false
|
"isPending": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -10,8 +10,6 @@
|
|||||||
"framework": "test",
|
"framework": "test",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "testProject.snowball.xyz"
|
"subDomain": "testProject.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -25,8 +23,6 @@
|
|||||||
"framework": "test-2",
|
"framework": "test-2",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "testProject-2.snowball.xyz"
|
"subDomain": "testProject-2.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -40,8 +36,6 @@
|
|||||||
"framework": "test-3",
|
"framework": "test-3",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "iglootools.snowball.xyz"
|
"subDomain": "iglootools.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -55,8 +49,6 @@
|
|||||||
"framework": "test-4",
|
"framework": "test-4",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "iglootools-2.snowball.xyz"
|
"subDomain": "iglootools-2.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,8 +62,6 @@
|
|||||||
"framework": "test-5",
|
"framework": "test-5",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"applicationDeploymentRequestId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"subDomain": "snowball-2.snowball.xyz"
|
"subDomain": "snowball-2.snowball.xyz"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
|
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
|
||||||
"name": "Snowball",
|
"name": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
||||||
"email": "snowball@snowballtools.xyz",
|
"email": "snowball@snowballtools.xyz",
|
||||||
"isVerified": true
|
"isVerified": true,
|
||||||
|
"ethAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "e505b212-8da6-48b2-9614-098225dab34b",
|
"id": "e505b212-8da6-48b2-9614-098225dab34b",
|
||||||
"name": "Alice Anderson",
|
"name": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
|
||||||
"email": "alice@snowballtools.xyz",
|
"email": "alice@snowballtools.xyz",
|
||||||
"isVerified": true
|
"isVerified": true,
|
||||||
|
"ethAddress": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cd892fad-9138-4aa2-a62c-414a32776ea7",
|
"id": "cd892fad-9138-4aa2-a62c-414a32776ea7",
|
||||||
"name": "Bob Banner",
|
"name": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a",
|
||||||
"email": "bob@snowballtools.xyz",
|
"email": "bob@snowballtools.xyz",
|
||||||
"isVerified": true
|
"isVerified": true,
|
||||||
|
"ethAddress": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import * as fs from 'fs/promises';
|
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
@@ -11,7 +10,12 @@ import { EnvironmentVariable } from '../src/entity/EnvironmentVariable';
|
|||||||
import { Domain } from '../src/entity/Domain';
|
import { Domain } from '../src/entity/Domain';
|
||||||
import { ProjectMember } from '../src/entity/ProjectMember';
|
import { ProjectMember } from '../src/entity/ProjectMember';
|
||||||
import { Deployment } from '../src/entity/Deployment';
|
import { Deployment } from '../src/entity/Deployment';
|
||||||
import { getConfig } from '../src/utils';
|
import {
|
||||||
|
checkFileExists,
|
||||||
|
getConfig,
|
||||||
|
getEntities,
|
||||||
|
loadAndSaveData
|
||||||
|
} from '../src/utils';
|
||||||
import { Config } from '../src/config';
|
import { Config } from '../src/config';
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||||
|
|
||||||
@@ -27,43 +31,35 @@ const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json';
|
|||||||
const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
|
const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json';
|
||||||
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
|
const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json';
|
||||||
|
|
||||||
const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, filePath: string, relations?: any | undefined) => {
|
|
||||||
const entitiesData = await fs.readFile(filePath, 'utf-8');
|
|
||||||
const entities = JSON.parse(entitiesData);
|
|
||||||
const entityRepository = dataSource.getRepository(entityType);
|
|
||||||
|
|
||||||
const savedEntity:Entity[] = [];
|
|
||||||
|
|
||||||
for (const entityData of entities) {
|
|
||||||
let entity = entityRepository.create(entityData as DeepPartial<Entity>);
|
|
||||||
|
|
||||||
if (relations) {
|
|
||||||
for (const field in relations) {
|
|
||||||
const valueIndex = String(field + 'Index');
|
|
||||||
|
|
||||||
entity = {
|
|
||||||
...entity,
|
|
||||||
[field]: relations[field][entityData[valueIndex]]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const dbEntity = await entityRepository.save(entity);
|
|
||||||
savedEntity.push(dbEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return savedEntity;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateTestData = async (dataSource: DataSource) => {
|
const generateTestData = async (dataSource: DataSource) => {
|
||||||
const savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH));
|
const userEntities = await getEntities(
|
||||||
const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
path.resolve(__dirname, USER_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedUsers = await loadAndSaveData(User, dataSource, userEntities);
|
||||||
|
|
||||||
|
const orgEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, ORGANIZATION_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedOrgs = await loadAndSaveData(
|
||||||
|
Organization,
|
||||||
|
dataSource,
|
||||||
|
orgEntities
|
||||||
|
);
|
||||||
|
|
||||||
const projectRelations = {
|
const projectRelations = {
|
||||||
owner: savedUsers,
|
owner: savedUsers,
|
||||||
organization: savedOrgs
|
organization: savedOrgs
|
||||||
};
|
};
|
||||||
|
|
||||||
const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations);
|
const projectEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, PROJECT_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedProjects = await loadAndSaveData(
|
||||||
|
Project,
|
||||||
|
dataSource,
|
||||||
|
projectEntities,
|
||||||
|
projectRelations
|
||||||
|
);
|
||||||
|
|
||||||
const domainRepository = dataSource.getRepository(Domain);
|
const domainRepository = dataSource.getRepository(Domain);
|
||||||
|
|
||||||
@@ -71,14 +67,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH), domainPrimaryRelations);
|
const primaryDomainsEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH)
|
||||||
|
);
|
||||||
|
const savedPrimaryDomains = await loadAndSaveData(
|
||||||
|
Domain,
|
||||||
|
dataSource,
|
||||||
|
primaryDomainsEntities,
|
||||||
|
domainPrimaryRelations
|
||||||
|
);
|
||||||
|
|
||||||
const domainRedirectedRelations = {
|
const domainRedirectedRelations = {
|
||||||
project: savedProjects,
|
project: savedProjects,
|
||||||
redirectTo: savedPrimaryDomains
|
redirectTo: savedPrimaryDomains
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH), domainRedirectedRelations);
|
const redirectDomainsEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
Domain,
|
||||||
|
dataSource,
|
||||||
|
redirectDomainsEntities,
|
||||||
|
domainRedirectedRelations
|
||||||
|
);
|
||||||
|
|
||||||
const savedDomains = await domainRepository.find();
|
const savedDomains = await domainRepository.find();
|
||||||
|
|
||||||
@@ -87,14 +99,30 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
organization: savedOrgs
|
organization: savedOrgs
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(UserOrganization, dataSource, path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), userOrganizationRelations);
|
const userOrganizationsEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
UserOrganization,
|
||||||
|
dataSource,
|
||||||
|
userOrganizationsEntities,
|
||||||
|
userOrganizationRelations
|
||||||
|
);
|
||||||
|
|
||||||
const projectMemberRelations = {
|
const projectMemberRelations = {
|
||||||
member: savedUsers,
|
member: savedUsers,
|
||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(ProjectMember, dataSource, path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH), projectMemberRelations);
|
const projectMembersEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
ProjectMember,
|
||||||
|
dataSource,
|
||||||
|
projectMembersEntities,
|
||||||
|
projectMemberRelations
|
||||||
|
);
|
||||||
|
|
||||||
const deploymentRelations = {
|
const deploymentRelations = {
|
||||||
project: savedProjects,
|
project: savedProjects,
|
||||||
@@ -102,23 +130,29 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
createdBy: savedUsers
|
createdBy: savedUsers
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(Deployment, dataSource, path.resolve(__dirname, DEPLOYMENT_DATA_PATH), deploymentRelations);
|
const deploymentsEntities = await getEntities(
|
||||||
|
path.resolve(__dirname, DEPLOYMENT_DATA_PATH)
|
||||||
|
);
|
||||||
|
await loadAndSaveData(
|
||||||
|
Deployment,
|
||||||
|
dataSource,
|
||||||
|
deploymentsEntities,
|
||||||
|
deploymentRelations
|
||||||
|
);
|
||||||
|
|
||||||
const environmentVariableRelations = {
|
const environmentVariableRelations = {
|
||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAndSaveData(EnvironmentVariable, dataSource, path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH), environmentVariableRelations);
|
const environmentVariablesEntities = await getEntities(
|
||||||
};
|
path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)
|
||||||
|
);
|
||||||
const checkFileExists = async (filePath: string) => {
|
await loadAndSaveData(
|
||||||
try {
|
EnvironmentVariable,
|
||||||
await fs.access(filePath, fs.constants.F_OK);
|
dataSource,
|
||||||
return true;
|
environmentVariablesEntities,
|
||||||
} catch (err) {
|
environmentVariableRelations
|
||||||
log(err);
|
);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
|
|||||||
@@ -11,22 +11,30 @@ const log = debug('snowball:initialize-registry');
|
|||||||
const DENOM = 'aphoton';
|
const DENOM = 'aphoton';
|
||||||
const BOND_AMOUNT = '1000000000';
|
const BOND_AMOUNT = '1000000000';
|
||||||
|
|
||||||
// TODO: Get authority names from args
|
|
||||||
const AUTHORITY_NAMES = ['snowballtools', 'cerc-io'];
|
|
||||||
|
|
||||||
async function main () {
|
async function main () {
|
||||||
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||||
|
|
||||||
|
// TODO: Get authority names from args
|
||||||
|
const authorityNames = ['snowballtools', registryConfig.authority];
|
||||||
|
|
||||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
||||||
|
|
||||||
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
||||||
log('bondId:', bondId);
|
log('bondId:', bondId);
|
||||||
await registry.createBond({ denom: DENOM, amount: BOND_AMOUNT }, registryConfig.privateKey, registryConfig.fee);
|
await registry.createBond(
|
||||||
|
{ denom: DENOM, amount: BOND_AMOUNT },
|
||||||
|
registryConfig.privateKey,
|
||||||
|
registryConfig.fee
|
||||||
|
);
|
||||||
|
|
||||||
for await (const name of AUTHORITY_NAMES) {
|
for await (const name of authorityNames) {
|
||||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
|
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
|
||||||
log('Reserved authority name:', name);
|
log('Reserved authority name:', name);
|
||||||
await registry.setAuthorityBond({ name, bondId }, registryConfig.privateKey, registryConfig.fee);
|
await registry.setAuthorityBond(
|
||||||
|
{ name, bondId },
|
||||||
|
registryConfig.privateKey,
|
||||||
|
registryConfig.fee
|
||||||
|
);
|
||||||
log(`Bond ${bondId} set for authority ${name}`);
|
log(`Bond ${bondId} set for authority ${name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,20 @@ import path from 'path';
|
|||||||
import { Registry } from '@cerc-io/laconic-sdk';
|
import { Registry } from '@cerc-io/laconic-sdk';
|
||||||
|
|
||||||
import { Config } from '../src/config';
|
import { Config } from '../src/config';
|
||||||
import { DEFAULT_CONFIG_FILE_PATH, PROJECT_DOMAIN } from '../src/constants';
|
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-deploy-records');
|
const log = debug('snowball:publish-deploy-records');
|
||||||
|
|
||||||
async function main () {
|
async function main () {
|
||||||
const { registryConfig, database } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||||
|
|
||||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
const registry = new Registry(
|
||||||
|
registryConfig.gqlEndpoint,
|
||||||
|
registryConfig.restEndpoint,
|
||||||
|
registryConfig.chainId
|
||||||
|
);
|
||||||
|
|
||||||
const dataSource = new DataSource({
|
const dataSource = new DataSource({
|
||||||
type: 'better-sqlite3',
|
type: 'better-sqlite3',
|
||||||
@@ -36,7 +40,7 @@ async function main () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for await (const deployment of deployments) {
|
for await (const deployment of deployments) {
|
||||||
const url = `${deployment.project.name}-${deployment.id}.${PROJECT_DOMAIN}`;
|
const url = `${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
|
||||||
|
|
||||||
const applicationDeploymentRecord = {
|
const applicationDeploymentRecord = {
|
||||||
type: 'ApplicationDeploymentRecord',
|
type: 'ApplicationDeploymentRecord',
|
||||||
@@ -53,7 +57,7 @@ async function main () {
|
|||||||
so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
|
so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
request: deployment.project.applicationDeploymentRequestId,
|
request: deployment.applicationDeploymentRequestId,
|
||||||
url
|
url
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# deployer
|
||||||
|
|
||||||
|
- Install dependencies
|
||||||
|
```bash
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
brew install jq # if you do not have jq installed already
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example of how to make the necessary deploy edits [here](https://github.com/snowball-tools/snowballtools-base/pull/131/files).
|
||||||
|
|
||||||
|
- Replace variables in the following files
|
||||||
|
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||||
|
- update the name & application version numbers
|
||||||
|
- `<CURRENT_DATE_TIME>`: Replace with current time which can be generated by command `date -u`
|
||||||
|
```yml
|
||||||
|
# Example
|
||||||
|
record:
|
||||||
|
...
|
||||||
|
meta:
|
||||||
|
note: Added by Snowball @ Friday 23 February 2024 06:35:50 AM UTC
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- Update record version in [records/application-record.yml](records/application-record.yml)
|
||||||
|
```yml
|
||||||
|
record:
|
||||||
|
type: ApplicationRecord
|
||||||
|
version: <NEW_VERSION>
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- Update commit hash in the following places:
|
||||||
|
- [records/application-record.yml](records/application-record.yml)
|
||||||
|
```yml
|
||||||
|
record:
|
||||||
|
...
|
||||||
|
repository_ref: <COMMIT_HASH>
|
||||||
|
...
|
||||||
|
```
|
||||||
|
- [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||||
|
```yml
|
||||||
|
record:
|
||||||
|
...
|
||||||
|
meta:
|
||||||
|
...
|
||||||
|
repository_ref: <COMMIT_HASH>
|
||||||
|
```
|
||||||
|
- [deploy-frontend.sh](deploy-frontend.sh)
|
||||||
|
Also be sure to update the app version
|
||||||
|
```bash
|
||||||
|
...
|
||||||
|
RCD_APP_VERSION="<NEW_VERSION>"
|
||||||
|
REPO_REF="<COMMIT_HASH>"
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run script to deploy app
|
||||||
|
```
|
||||||
|
./deploy-frontend.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- Commit the updated [ApplicationRecord](records/application-record.yml) and [ApplicationDeploymentRequest](records/application-deployment-request.yml) files to the repository
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
|
||||||
|
```yml
|
||||||
|
record:
|
||||||
|
...
|
||||||
|
config:
|
||||||
|
env:
|
||||||
|
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
|
||||||
|
|
||||||
|
## Troubleshoot
|
||||||
|
|
||||||
|
- Check deployment status [here](https://console.laconic.com/deployer).
|
||||||
|
- Check records [here](https://console.laconic.com/#/registry).
|
||||||
|
|
||||||
|
- If deployment fails due to low bond balance
|
||||||
|
- Check balances
|
||||||
|
```bash
|
||||||
|
# Account balance
|
||||||
|
yarn laconic cns account get
|
||||||
|
|
||||||
|
# Bond balance
|
||||||
|
yarn laconic cns bond get --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
|
||||||
|
```
|
||||||
|
- Command to refill bond
|
||||||
|
```bash
|
||||||
|
yarn laconic cns bond refill --id 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac --type aphoton --quantity 10000000
|
||||||
|
```
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
cns:
|
||||||
|
restEndpoint: http://console.laconic.com:1317
|
||||||
|
gqlEndpoint: http://console.laconic.com:9473/api
|
||||||
|
chainId: laconic_9000-1
|
||||||
|
gas: 1200000
|
||||||
|
fees: 200000aphoton
|
||||||
|
userKey: 0524fc22ea0a12e6c5cc4cfe08e73c95dffd0ab5ed72a59f459ed33134fa3b16
|
||||||
|
bondId: 8fcf44b2f326b4b63ac57547777f1c78b7d494e5966e508f09001af53cb440ac
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Repository URL
|
||||||
|
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||||
|
|
||||||
|
# Get the latest commit hash from the repository
|
||||||
|
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
||||||
|
|
||||||
|
# Extract version from ../frontend/package.json
|
||||||
|
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
||||||
|
|
||||||
|
# Current date and time for note
|
||||||
|
CURRENT_DATE_TIME=$(date -u)
|
||||||
|
|
||||||
|
# Increment application-record version
|
||||||
|
APPLICATION_RECORD_FILE="./records/application-record.yml"
|
||||||
|
if [ -f "$APPLICATION_RECORD_FILE" ]; then
|
||||||
|
# Extract current version and increment it
|
||||||
|
CURRENT_VERSION=$(grep 'version:' $APPLICATION_RECORD_FILE | head -1 | awk '{print $2}')
|
||||||
|
IFS='.' read -ra ADDR <<< "$CURRENT_VERSION"
|
||||||
|
VERSION_NUMBER=${ADDR[2]}
|
||||||
|
NEW_VERSION_NUMBER=$((VERSION_NUMBER + 1))
|
||||||
|
NEW_APPLICATION_VERSION="${ADDR[0]}.${ADDR[1]}.$NEW_VERSION_NUMBER"
|
||||||
|
else
|
||||||
|
# If file does not exist, start from version 0.0.1
|
||||||
|
NEW_APPLICATION_VERSION="0.0.1"
|
||||||
|
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_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||||
|
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
||||||
|
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||||
|
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||||
|
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
||||||
|
LACONIC_HOSTED_CONFIG_app_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
|
||||||
|
cat > ./records/application-record.yml <<EOF
|
||||||
|
record:
|
||||||
|
type: ApplicationRecord
|
||||||
|
version: $NEW_APPLICATION_VERSION
|
||||||
|
repository_ref: $LATEST_HASH
|
||||||
|
repository: ["$REPO_URL"]
|
||||||
|
app_type: webapp
|
||||||
|
name: snowballtools-base-frontend
|
||||||
|
app_version: $PACKAGE_VERSION
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Files generated successfully."
|
||||||
|
|
||||||
|
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||||
|
|
||||||
|
RECORD_FILE=records/application-record.yml
|
||||||
|
CONFIG_FILE=config.yml
|
||||||
|
|
||||||
|
# Publish ApplicationRecord
|
||||||
|
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||||
|
echo "ApplicationRecord published"
|
||||||
|
echo $RECORD_ID
|
||||||
|
|
||||||
|
# Set name to record
|
||||||
|
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
|
||||||
|
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
||||||
|
# Set name if latest release
|
||||||
|
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||||
|
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||||
|
|
||||||
|
# Check if record found for REGISTRY_APP_CRN
|
||||||
|
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
||||||
|
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||||
|
echo "No record found for $REGISTRY_APP_CRN."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RECORD_FILE=records/application-deployment-request.yml
|
||||||
|
|
||||||
|
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||||
|
echo "ApplicationDeploymentRequest published"
|
||||||
|
echo $DEPLOYMENT_REQUEST_ID
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "deployer",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@cerc-io/laconic-registry-cli": "^0.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
record:
|
||||||
|
type: ApplicationDeploymentRequest
|
||||||
|
version: '1.0.0'
|
||||||
|
name: snowballtools-base-frontend@0.1.8
|
||||||
|
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
|
||||||
|
dns: dashboard
|
||||||
|
config:
|
||||||
|
env:
|
||||||
|
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||||
|
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
||||||
|
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||||
|
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||||
|
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
||||||
|
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||||
|
meta:
|
||||||
|
note: Added by Snowball @ Thu Feb 29 07:46:48 UTC 2024
|
||||||
|
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||||
|
repository_ref: a7810a34c90d5bfe0c6cc0697ad9aaf79b1c41d7
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
record:
|
||||||
|
type: ApplicationRecord
|
||||||
|
version: 0.0.27
|
||||||
|
repository_ref: a7810a34c90d5bfe0c6cc0697ad9aaf79b1c41d7
|
||||||
|
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||||
|
app_type: webapp
|
||||||
|
name: snowballtools-base-frontend
|
||||||
|
app_version: 0.1.8
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
|
|
||||||
|
|
||||||
REACT_APP_GITHUB_CLIENT_ID =
|
|
||||||
REACT_APP_GITHUB_TEMPLATE_REPO =
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||||
|
|
||||||
|
REACT_APP_GITHUB_CLIENT_ID =
|
||||||
|
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
|
||||||
|
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
|
||||||
|
|
||||||
|
REACT_APP_WALLET_CONNECT_ID =
|
||||||
@@ -16,5 +16,10 @@
|
|||||||
"plugin:react/recommended",
|
"plugin:react/recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:prettier/recommended"
|
"plugin:prettier/recommended"
|
||||||
]
|
],
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
// eslint extension options
|
|
||||||
"eslint.enable": true,
|
|
||||||
"eslint.validate": [
|
|
||||||
"javascript",
|
|
||||||
"javascriptreact",
|
|
||||||
"typescript",
|
|
||||||
"typescriptreact"
|
|
||||||
],
|
|
||||||
"css.customData": [".vscode/tailwind.json"],
|
|
||||||
// prettier extension setting
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[javascriptreact]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[typescriptreact]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"editor.rulers": [80],
|
|
||||||
"editor.codeActionsOnSave": [
|
|
||||||
"source.addMissingImports",
|
|
||||||
"source.fixAll",
|
|
||||||
"source.organizeImports"
|
|
||||||
],
|
|
||||||
// Show in vscode "Problems" tab when there are errors
|
|
||||||
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
|
|
||||||
// Use absolute import for typescript files
|
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
|
||||||
// IntelliSense for taiwind variants
|
|
||||||
"tailwindCSS.experimental.classRegex": [
|
|
||||||
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="500" height="500" fill="#0F86F5"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1,13 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||||
"@fontsource/inter": "^5.0.16",
|
"@fontsource/inter": "^5.0.16",
|
||||||
"@material-tailwind/react": "^2.1.7",
|
"@material-tailwind/react": "^2.1.7",
|
||||||
"@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-popover": "^1.0.7",
|
||||||
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
|
"@tanstack/react-query": "^5.22.2",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
@@ -15,10 +22,13 @@
|
|||||||
"@types/node": "^16.18.68",
|
"@types/node": "^16.18.68",
|
||||||
"@types/react": "^18.2.42",
|
"@types/react": "^18.2.42",
|
||||||
"@types/react-dom": "^18.2.17",
|
"@types/react-dom": "^18.2.17",
|
||||||
|
"@web3modal/siwe": "^4.0.5",
|
||||||
|
"@web3modal/wagmi": "^4.0.5",
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
|
"axios": "^1.6.7",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
"downshift": "^8.2.3",
|
"downshift": "^8.3.2",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"gql-client": "^1.0.0",
|
"gql-client": "^1.0.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
@@ -35,10 +45,13 @@
|
|||||||
"react-router-dom": "^6.20.1",
|
"react-router-dom": "^6.20.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-timer-hook": "^3.0.7",
|
"react-timer-hook": "^3.0.7",
|
||||||
|
"siwe": "^2.1.4",
|
||||||
"tailwind-variants": "^0.2.0",
|
"tailwind-variants": "^0.2.0",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"usehooks-ts": "^2.10.0",
|
"usehooks-ts": "^2.10.0",
|
||||||
"vertical-stepper-nav": "^1.0.2",
|
"vertical-stepper-nav": "^1.0.2",
|
||||||
|
"viem": "^2.7.11",
|
||||||
|
"wagmi": "^2.5.7",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
|
<TileColor>#2d89ef</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
|
After Width: | Height: | Size: 674 B |
|
After Width: | Height: | Size: 989 B |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 7.2 KiB |
@@ -2,42 +2,34 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="description" content="snowball tools dashboard" />
|
||||||
<meta
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
<!--
|
<link
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
rel="apple-touch-icon"
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
sizes="180x180"
|
||||||
-->
|
href="%PUBLIC_URL%/apple-touch-icon.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="%PUBLIC_URL%/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="%PUBLIC_URL%/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
|
||||||
|
<meta name="msapplication-TileColor" content="#2d89ef" />
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>Snowball</title>
|
<title>Snowball</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="500" height="500" fill="#0F86F5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/><path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/><path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "Snowball Tools Dashboard",
|
||||||
|
"short_name": "snowball tools",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M-12.6045 1.27694L-19 3.9698H37.5L34.8955 2.66266L31.6045 1.27694C29.6192 0.441052 27.3808 0.441052 25.3955 1.27694L22.1045 2.66266C20.1192 3.49855 17.8808 3.49855 15.8955 2.66266L12.6045 1.27694C10.6192 0.441052 8.38078 0.441052 6.39555 1.27694L3.10446 2.66266C1.11922 3.49855 -1.11922 3.49855 -3.10445 2.66266L-6.39554 1.27694C-8.38078 0.441052 -10.6192 0.441052 -12.6045 1.27694Z" fill="currentColor"/>
|
||||||
|
<path d="M25.7836 1.58773L22.4925 2.97346C20.3832 3.86159 18.0049 3.86159 15.8955 2.97346L12.6045 1.58774C10.7433 0.804089 8.64476 0.804088 6.7836 1.58773L3.49251 2.97346C1.3832 3.86159 -0.99514 3.86159 -3.10445 2.97346L-6.39554 1.58774C-8.2567 0.804089 -10.3552 0.804088 -12.2164 1.58773L-18.6119 4.2806L-19 3.35896L-12.6045 0.666099C-10.4951 -0.222033 -8.1168 -0.222033 -6.00749 0.6661L-2.7164 2.05182C-0.855238 2.83547 1.2433 2.83547 3.10446 2.05182L6.39555 0.666099C8.50486 -0.222033 10.8832 -0.222033 12.9925 0.6661L16.2836 2.05182C18.1448 2.83547 20.2433 2.83547 22.1045 2.05182L25.3955 0.666099L25.7836 1.58773Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1177_19315)">
|
||||||
|
<path d="M-12.6045 1.27694L-19 3.9698H37.5L34.8955 2.66266L31.6045 1.27694C29.6192 0.441052 27.3808 0.441052 25.3955 1.27694L22.1045 2.66266C20.1192 3.49855 17.8808 3.49855 15.8955 2.66266L12.6045 1.27694C10.6192 0.441052 8.38078 0.441052 6.39555 1.27694L3.10446 2.66266C1.11922 3.49855 -1.11922 3.49855 -3.10445 2.66266L-6.39554 1.27694C-8.38078 0.441052 -10.6192 0.441052 -12.6045 1.27694Z" fill="url(#paint0_linear_1177_19315)"/>
|
||||||
|
<path d="M25.7836 1.58773L22.4925 2.97346C20.3832 3.86159 18.0049 3.86159 15.8955 2.97346L12.6045 1.58774C10.7433 0.804089 8.64476 0.804088 6.7836 1.58773L3.49251 2.97346C1.3832 3.86159 -0.99514 3.86159 -3.10445 2.97346L-6.39554 1.58774C-8.2567 0.804089 -10.3552 0.804088 -12.2164 1.58773L-18.6119 4.2806L-19 3.35896L-12.6045 0.666099C-10.4951 -0.222033 -8.1168 -0.222033 -6.00749 0.6661L-2.7164 2.05182C-0.855238 2.83547 1.2433 2.83547 3.10446 2.05182L6.39555 0.666099C8.50486 -0.222033 10.8832 -0.222033 12.9925 0.6661L16.2836 2.05182C18.1448 2.83547 20.2433 2.83547 22.1045 2.05182L25.3955 0.666099L25.7836 1.58773Z" fill="#DBEBF9"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_1177_19315" x1="9.25" y1="0.650024" x2="9.25" y2="3.9698" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#EBF6FF"/>
|
||||||
|
<stop offset="1" stop-color="#F7FCFF" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="clip0_1177_19315">
|
||||||
|
<rect width="19" height="4" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1175_19292)">
|
||||||
|
<path d="M-9.5 0L-3.10445 2.69286C-1.11922 3.52875 1.11922 3.52875 3.10446 2.69286L6.39555 1.30714C8.38078 0.47125 10.6192 0.47125 12.6045 1.30714L15.8955 2.69286C17.8808 3.52875 20.1192 3.52875 22.1045 2.69286L28.5 0" stroke="#082F56" stroke-opacity="0.1"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1175_19292">
|
||||||
|
<rect width="19" height="4" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 507 B |
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
import OrgSlug from './pages/OrgSlug';
|
|
||||||
import Projects from './pages/org-slug';
|
import Projects from './pages/org-slug';
|
||||||
import Settings from './pages/org-slug/Settings';
|
import Settings from './pages/org-slug/Settings';
|
||||||
import {
|
import {
|
||||||
@@ -9,13 +8,14 @@ import {
|
|||||||
projectsRoutesWithoutSearch,
|
projectsRoutesWithoutSearch,
|
||||||
} from './pages/org-slug/projects/routes';
|
} from './pages/org-slug/projects/routes';
|
||||||
import ProjectSearchLayout from './layouts/ProjectSearch';
|
import ProjectSearchLayout from './layouts/ProjectSearch';
|
||||||
import { OctokitProvider } from './context/OctokitContext';
|
|
||||||
import Index from './pages';
|
import Index from './pages';
|
||||||
|
import Login from './pages/Login';
|
||||||
|
import { DashboardLayout } from 'pages/org-slug/layout';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: ':orgSlug',
|
path: ':orgSlug',
|
||||||
element: <OrgSlug />,
|
element: <DashboardLayout />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
element: <ProjectSearchLayout />,
|
element: <ProjectSearchLayout />,
|
||||||
@@ -44,14 +44,14 @@ const router = createBrowserRouter([
|
|||||||
path: '/',
|
path: '/',
|
||||||
element: <Index />,
|
element: <Index />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
element: <Login />,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return <RouterProvider router={router} />;
|
||||||
<OctokitProvider>
|
|
||||||
<RouterProvider router={router} />
|
|
||||||
</OctokitProvider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "Progressive Web App (PWA)",
|
|
||||||
"icon": "^"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"name": "Kotlin",
|
|
||||||
"icon": "^"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "3",
|
|
||||||
"name": "React Native",
|
|
||||||
"icon": "^"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "4",
|
|
||||||
"name": "Swift",
|
|
||||||
"icon": "^"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "5",
|
|
||||||
"name": "Web app",
|
|
||||||
"icon": "^"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Progressive Web App (PWA)',
|
||||||
|
icon: 'pwa',
|
||||||
|
repoFullName: `${process.env.REACT_APP_GITHUB_PWA_TEMPLATE_REPO}`,
|
||||||
|
isComingSoon: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Image Upload PWA',
|
||||||
|
icon: 'pwa',
|
||||||
|
repoFullName: `${process.env.REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
|
||||||
|
isComingSoon: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Kotlin',
|
||||||
|
icon: 'kotlin',
|
||||||
|
repoFullName: '',
|
||||||
|
isComingSoon: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: 'React Native',
|
||||||
|
icon: 'react-native',
|
||||||
|
repoFullName: '',
|
||||||
|
isComingSoon: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
name: 'Swift',
|
||||||
|
icon: 'swift',
|
||||||
|
repoFullName: '',
|
||||||
|
isComingSoon: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -126,7 +126,8 @@ const DatePicker = ({
|
|||||||
crossOrigin={undefined}
|
crossOrigin={undefined}
|
||||||
/>
|
/>
|
||||||
</PopoverHandler>
|
</PopoverHandler>
|
||||||
<PopoverContent>
|
{/* TODO: Figure out what placeholder is for */}
|
||||||
|
<PopoverContent placeholder={''}>
|
||||||
{mode === 'single' && (
|
{mode === 'single' && (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
mode="single"
|
mode="single"
|
||||||
@@ -145,19 +146,23 @@ const DatePicker = ({
|
|||||||
/>
|
/>
|
||||||
<HorizontalLine />
|
<HorizontalLine />
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
{/* TODO: Figure out what placeholder is for */}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="rounded-full mr-2"
|
className="rounded-full mr-2"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
{/* TODO: Figure out what placeholder is for */}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="rounded-full"
|
className="rounded-full"
|
||||||
color="gray"
|
color="gray"
|
||||||
onClick={() => handleRangeSelect()}
|
onClick={() => handleRangeSelect()}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
Select
|
Select
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const HorizontalLine = () => {
|
const HorizontalLine = () => {
|
||||||
return <hr className="h-px bg-gray-300 border-0" />;
|
return <hr className="h-px bg-gray-100 border-0" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HorizontalLine;
|
export default HorizontalLine;
|
||||||
|
|||||||
@@ -1,33 +1,23 @@
|
|||||||
import React, { forwardRef, RefAttributes } from 'react';
|
import React, { forwardRef, RefAttributes } from 'react';
|
||||||
|
|
||||||
import { Input, InputProps } from '@material-tailwind/react';
|
import { SearchIcon } from './shared/CustomIcon';
|
||||||
|
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 }, ref) => {
|
> = ({ value, onChange, placeholder = 'Search', ...props }) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex w-full gap-2">
|
<div className="relative flex w-full">
|
||||||
<Input
|
<Input
|
||||||
|
leftIcon={<SearchIcon />}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
type="search"
|
type="search"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
containerProps={{
|
appearance={'borderless'}
|
||||||
className: 'min-w-[288px]',
|
|
||||||
}}
|
|
||||||
className="!border-t-blue-gray-300 pl-9 placeholder:text-blue-gray-300 focus:!border-blue-gray-300"
|
|
||||||
labelProps={{
|
|
||||||
className: 'before:content-none after:content-none',
|
|
||||||
}}
|
|
||||||
// TODO: Debug issue: https://github.com/creativetimofficial/material-tailwind/issues/427
|
|
||||||
crossOrigin={undefined}
|
|
||||||
{...props}
|
{...props}
|
||||||
inputRef={ref}
|
|
||||||
/>
|
/>
|
||||||
<div className="!absolute left-3 top-[13px]">
|
|
||||||
<i>^</i>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
|
|
||||||
import { Organization } from 'gql-client';
|
|
||||||
|
|
||||||
import { Typography, Option } from '@material-tailwind/react';
|
|
||||||
|
|
||||||
import { useGQLClient } from '../context/GQLClientContext';
|
|
||||||
import AsyncSelect from './shared/AsyncSelect';
|
|
||||||
|
|
||||||
const Sidebar = () => {
|
|
||||||
const { orgSlug } = useParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const client = useGQLClient();
|
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-full p-4">
|
|
||||||
<div className="grow">
|
|
||||||
<div>
|
|
||||||
<Link to={`/${orgSlug}`}>
|
|
||||||
<h3 className="text-black text-2xl">Snowball</h3>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<AsyncSelect
|
|
||||||
className="bg-white py-2"
|
|
||||||
value={selectedOrgSlug}
|
|
||||||
onChange={(value) => {
|
|
||||||
setSelectedOrgSlug(value!);
|
|
||||||
navigate(`/${value}`);
|
|
||||||
}}
|
|
||||||
selected={(_, index) => (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div>^</div>
|
|
||||||
<div>
|
|
||||||
<span>{organizations[index!]?.name}</span>
|
|
||||||
<Typography>Organization</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* TODO: Show label organization and manage in option */}
|
|
||||||
{organizations.map((org) => (
|
|
||||||
<Option key={org.id} value={org.slug}>
|
|
||||||
^ {org.name}
|
|
||||||
{org.slug === selectedOrgSlug && <p className="float-right">^</p>}
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</AsyncSelect>
|
|
||||||
<div>
|
|
||||||
<NavLink
|
|
||||||
to={`/${orgSlug}`}
|
|
||||||
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
|
|
||||||
>
|
|
||||||
<Typography>Projects</Typography>
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<NavLink
|
|
||||||
to={`/${orgSlug}/settings`}
|
|
||||||
className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
|
|
||||||
>
|
|
||||||
<Typography>Settings</Typography>
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grow flex flex-col justify-end">
|
|
||||||
<div>Documentation</div>
|
|
||||||
<div>Support</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Sidebar;
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { Project } from 'gql-client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Menu,
|
|
||||||
MenuHandler,
|
|
||||||
MenuList,
|
|
||||||
MenuItem,
|
|
||||||
Typography,
|
|
||||||
Avatar,
|
|
||||||
} from '@material-tailwind/react';
|
|
||||||
|
|
||||||
import { relativeTimeMs } from '../../utils/time';
|
|
||||||
|
|
||||||
interface ProjectCardProps {
|
|
||||||
project: Project;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg shadow">
|
|
||||||
<div className="flex gap-2 p-2 items-center">
|
|
||||||
<Avatar variant="rounded" src={project.icon || '/gray.png'} />
|
|
||||||
<div className="grow">
|
|
||||||
<Link to={`projects/${project.id}`}>
|
|
||||||
<Typography>{project.name}</Typography>
|
|
||||||
<Typography color="gray" variant="small">
|
|
||||||
{project.deployments[0]?.domain?.name ??
|
|
||||||
'No Production Deployment'}
|
|
||||||
</Typography>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<Menu placement="bottom-end">
|
|
||||||
<MenuHandler>
|
|
||||||
<button>...</button>
|
|
||||||
</MenuHandler>
|
|
||||||
<MenuList>
|
|
||||||
<MenuItem>^ Project settings</MenuItem>
|
|
||||||
<MenuItem className="text-red-500">^ Delete project</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
<div className="border-t-2 border-solid p-4 bg-gray-50">
|
|
||||||
{project.deployments.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<Typography variant="small" color="gray">
|
|
||||||
^ {project.deployments[0].commitMessage}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="small" color="gray">
|
|
||||||
{relativeTimeMs(project.deployments[0].createdAt)} on ^
|
|
||||||
{project.deployments[0].branch}
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Typography variant="small" color="gray">
|
|
||||||
No Production deployment
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectCard;
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const projectCardTheme = tv({
|
||||||
|
slots: {
|
||||||
|
wrapper: [
|
||||||
|
'bg-surface-card',
|
||||||
|
'shadow-card',
|
||||||
|
'rounded-2xl',
|
||||||
|
'flex',
|
||||||
|
'flex-col',
|
||||||
|
'group',
|
||||||
|
],
|
||||||
|
upperContent: ['px-4', 'py-4', 'flex', 'items-start', 'gap-3', 'relative'],
|
||||||
|
content: ['flex', 'flex-col', 'gap-1', 'flex-1'],
|
||||||
|
title: [
|
||||||
|
'text-sm',
|
||||||
|
'font-medium',
|
||||||
|
'text-elements-high-em',
|
||||||
|
'tracking-[-0.006em]',
|
||||||
|
],
|
||||||
|
description: ['text-xs', 'text-elements-low-em'],
|
||||||
|
icons: ['flex', 'items-center', 'gap-1'],
|
||||||
|
lowerContent: [
|
||||||
|
'transition-colors',
|
||||||
|
'duration-150',
|
||||||
|
'px-4',
|
||||||
|
'py-4',
|
||||||
|
'flex',
|
||||||
|
'flex-col',
|
||||||
|
'gap-2',
|
||||||
|
'rounded-b-2xl',
|
||||||
|
'group-hover:bg-surface-card-hovered',
|
||||||
|
],
|
||||||
|
latestDeployment: ['flex', 'items-center', 'gap-2'],
|
||||||
|
deploymentStatusContainer: [
|
||||||
|
'h-3',
|
||||||
|
'w-3',
|
||||||
|
'flex',
|
||||||
|
'items-center',
|
||||||
|
'justify-center',
|
||||||
|
],
|
||||||
|
deploymentStatus: ['w-1', 'h-1', 'rounded-full'],
|
||||||
|
deploymentName: ['text-xs', 'text-elements-low-em'],
|
||||||
|
deploymentText: [
|
||||||
|
'text-xs',
|
||||||
|
'text-elements-low-em',
|
||||||
|
'font-mono',
|
||||||
|
'flex',
|
||||||
|
'items-center',
|
||||||
|
'gap-2',
|
||||||
|
],
|
||||||
|
wavyBorder: [
|
||||||
|
'bg-surface-card',
|
||||||
|
'transition-colors',
|
||||||
|
'duration-150',
|
||||||
|
'group-hover:bg-surface-card-hovered',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
status: {
|
||||||
|
success: {
|
||||||
|
deploymentStatus: ['bg-emerald-500'],
|
||||||
|
},
|
||||||
|
'in-progress': {
|
||||||
|
deploymentStatus: ['bg-orange-400'],
|
||||||
|
},
|
||||||
|
failure: {
|
||||||
|
deploymentStatus: ['bg-rose-500'],
|
||||||
|
},
|
||||||
|
pending: {
|
||||||
|
deploymentStatus: ['bg-gray-500'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ProjectCardTheme = VariantProps<typeof projectCardTheme>;
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import React, {
|
||||||
|
ComponentPropsWithoutRef,
|
||||||
|
MouseEvent,
|
||||||
|
useCallback,
|
||||||
|
} from 'react';
|
||||||
|
import { ProjectCardTheme, projectCardTheme } from './ProjectCard.theme';
|
||||||
|
import { Project } from 'gql-client';
|
||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||||
|
import {
|
||||||
|
BranchIcon,
|
||||||
|
ClockIcon,
|
||||||
|
GitHubLogo,
|
||||||
|
HorizontalDotIcon,
|
||||||
|
WarningDiamondIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { relativeTimeMs } from 'utils/time';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Avatar } from 'components/shared/Avatar';
|
||||||
|
import { getInitials } from 'utils/geInitials';
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuHandler,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
} from '@material-tailwind/react';
|
||||||
|
|
||||||
|
export interface ProjectCardProps
|
||||||
|
extends ComponentPropsWithoutRef<'div'>,
|
||||||
|
ProjectCardTheme {
|
||||||
|
project: Project;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Update the whole component to use `Link` from `react-router-dom` and remove the `useNavigate` hook,
|
||||||
|
// currently it's not possible to use `Link` because the dot menu is not a direct child of the `Link` component
|
||||||
|
export const ProjectCard = ({
|
||||||
|
className,
|
||||||
|
project,
|
||||||
|
status = 'failure',
|
||||||
|
...props
|
||||||
|
}: ProjectCardProps) => {
|
||||||
|
const theme = projectCardTheme();
|
||||||
|
const hasDeployment = project.deployments.length > 0;
|
||||||
|
// TODO: Update this to use the actual status from the API
|
||||||
|
const hasError = status === 'failure';
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleOptionsClick = (
|
||||||
|
e: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>,
|
||||||
|
) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
navigate(`projects/${project.id}`);
|
||||||
|
}, [project.id, navigate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={theme.wrapper({ className })}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{/* Upper content */}
|
||||||
|
<div className={theme.upperContent()}>
|
||||||
|
{/* Icon container */}
|
||||||
|
<Avatar
|
||||||
|
size={48}
|
||||||
|
imageSrc={project.icon}
|
||||||
|
initials={getInitials(project.name)}
|
||||||
|
/>
|
||||||
|
{/* Title and website */}
|
||||||
|
<div className={theme.content()}>
|
||||||
|
<p className={theme.title()}>{project.name}</p>
|
||||||
|
<p className={theme.description()}>
|
||||||
|
{project.deployments[0]?.domain?.name ?? 'No domain'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Icons */}
|
||||||
|
<div className={theme.icons()}>
|
||||||
|
{hasError && <WarningDiamondIcon className="text-elements-danger" />}
|
||||||
|
<Menu placement="bottom-end">
|
||||||
|
<MenuHandler>
|
||||||
|
<Button
|
||||||
|
shape="default"
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
iconOnly
|
||||||
|
onClick={handleOptionsClick}
|
||||||
|
>
|
||||||
|
<HorizontalDotIcon />
|
||||||
|
</Button>
|
||||||
|
</MenuHandler>
|
||||||
|
<MenuList placeholder={''}>
|
||||||
|
<MenuItem placeholder={''}>Project settings</MenuItem>
|
||||||
|
<MenuItem className="text-red-500" placeholder={''}>
|
||||||
|
Delete project
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Wave */}
|
||||||
|
<WavyBorder variant="stroke-and-fill" className={theme.wavyBorder()} />
|
||||||
|
{/* Lower content */}
|
||||||
|
<div className={theme.lowerContent()}>
|
||||||
|
{/* Latest deployment */}
|
||||||
|
<div className={theme.latestDeployment()}>
|
||||||
|
{/* Dot icon */}
|
||||||
|
<div className={theme.deploymentStatusContainer()}>
|
||||||
|
<div className={theme.deploymentStatus({ status })} />
|
||||||
|
</div>
|
||||||
|
<p className={theme.deploymentText()}>
|
||||||
|
{hasDeployment
|
||||||
|
? project.deployments[0]?.commitMessage
|
||||||
|
: 'No production deployment'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Deployment and branch name */}
|
||||||
|
<div className={theme.deploymentText()}>
|
||||||
|
{hasDeployment ? (
|
||||||
|
<>
|
||||||
|
<GitHubLogo />
|
||||||
|
<span>{relativeTimeMs(project.deployments[0].createdAt)} on</span>
|
||||||
|
<BranchIcon />
|
||||||
|
<span>{project.deployments[0].branch}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ClockIcon />
|
||||||
|
<span>Created {relativeTimeMs(project.createdAt)}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './ProjectCard';
|
||||||
@@ -71,12 +71,13 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
|||||||
className={`absolute w-1/2 max-h-52 -mt-1 overflow-y-auto ${
|
className={`absolute w-1/2 max-h-52 -mt-1 overflow-y-auto ${
|
||||||
(!inputValue || !isOpen) && 'hidden'
|
(!inputValue || !isOpen) && 'hidden'
|
||||||
}`}
|
}`}
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
<List {...getMenuProps()}>
|
<List {...getMenuProps()}>
|
||||||
{items.length ? (
|
{items.length ? (
|
||||||
<>
|
<>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<Typography variant="small" color="gray">
|
<Typography variant="small" color="gray" placeholder={''}>
|
||||||
Suggestions
|
Suggestions
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,19 +85,25 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
|||||||
<ListItem
|
<ListItem
|
||||||
selected={highlightedIndex === index || selectedItem === item}
|
selected={highlightedIndex === index || selectedItem === item}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
placeholder={''}
|
||||||
{...getItemProps({ item, index })}
|
{...getItemProps({ item, index })}
|
||||||
>
|
>
|
||||||
<ListItemPrefix>
|
<ListItemPrefix placeholder={''}>
|
||||||
<Avatar src={item.icon || '/gray.png'} variant="rounded" />
|
<Avatar
|
||||||
|
src={item.icon || '/gray.png'}
|
||||||
|
variant="rounded"
|
||||||
|
placeholder={''}
|
||||||
|
/>
|
||||||
</ListItemPrefix>
|
</ListItemPrefix>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="h6" color="blue-gray">
|
<Typography variant="h6" color="blue-gray" placeholder={''}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="small"
|
variant="small"
|
||||||
color="gray"
|
color="gray"
|
||||||
className="font-normal"
|
className="font-normal"
|
||||||
|
placeholder={''}
|
||||||
>
|
>
|
||||||
{item.organization.name}
|
{item.organization.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -106,7 +113,9 @@ const ProjectSearchBar = ({ onChange }: ProjectsSearchProps) => {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<Typography>^ No projects matching this name</Typography>
|
<Typography placeholder={''}>
|
||||||
|
^ No projects matching this name
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { Button } from '@material-tailwind/react';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import OauthPopup from 'react-oauth-popup';
|
import OauthPopup from 'react-oauth-popup';
|
||||||
|
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
import ConnectAccountTabPanel from './ConnectAccountTabPanel';
|
import { Button } from 'components/shared/Button';
|
||||||
|
import {
|
||||||
|
GitIcon,
|
||||||
|
EllipsesIcon,
|
||||||
|
SnowballIcon,
|
||||||
|
GithubIcon,
|
||||||
|
GitTeaIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
import { IconWithFrame } from 'components/shared/IconWithFrame';
|
||||||
|
import { Heading } from 'components/shared/Heading';
|
||||||
|
|
||||||
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=${
|
||||||
@@ -14,43 +23,79 @@ interface ConnectAccountInterface {
|
|||||||
onAuth: (token: string) => void;
|
onAuth: (token: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
||||||
|
onAuth: onToken,
|
||||||
|
}: ConnectAccountInterface) => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
|
|
||||||
|
const { toast, dismiss } = useToast();
|
||||||
|
|
||||||
const handleCode = async (code: string) => {
|
const handleCode = async (code: string) => {
|
||||||
// Pass code to backend and get access token
|
// Pass code to backend and get access token
|
||||||
const {
|
const {
|
||||||
authenticateGitHub: { token },
|
authenticateGitHub: { token },
|
||||||
} = await client.authenticateGitHub(code);
|
} = await client.authenticateGitHub(code);
|
||||||
onToken(token);
|
onToken(token);
|
||||||
|
toast({
|
||||||
|
onDismiss: dismiss,
|
||||||
|
id: 'connected-to-github',
|
||||||
|
title: 'The Git account is connected.',
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Use correct height
|
// TODO: Use correct height
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-100 flex flex-col p-4 justify-center items-center text-center text-sm h-full rounded-2xl">
|
<div className="bg-gray-100 flex flex-col p-4 gap-7 justify-center items-center text-center text-sm h-full rounded-2xl">
|
||||||
<div>^</div>
|
<div className="flex flex-col items-center max-w-[420px]">
|
||||||
<div>
|
{/** Icons */}
|
||||||
<p>Connect to your git account</p>
|
<div className="w-52 h-16 justify-center items-center gap-4 inline-flex mb-7">
|
||||||
<p>
|
<IconWithFrame icon={<GitIcon />} />
|
||||||
Once connected, you can import a repository from your
|
<EllipsesIcon className="items-center gap-1.5 flex" />
|
||||||
<br />
|
<IconWithFrame className="bg-blue-400" icon={<SnowballIcon />} />
|
||||||
account or start with one of our templates.
|
</div>
|
||||||
</p>
|
{/** Text */}
|
||||||
|
<div className="flex flex-col gap-1.5 mb-6">
|
||||||
|
<Heading className="text-xl font-medium">
|
||||||
|
Connect to your Git account
|
||||||
|
</Heading>
|
||||||
|
<p className="text-center text-elements-mid-em">
|
||||||
|
Once connected, you can import a repository from your account or
|
||||||
|
start with one of our templates.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/** CTA Buttons */}
|
||||||
|
<div className="flex flex-col w-full sm:w-auto sm:flex-row gap-2 sm:gap-3">
|
||||||
|
<OauthPopup
|
||||||
|
url={GITHUB_OAUTH_URL}
|
||||||
|
onCode={handleCode}
|
||||||
|
onClose={() => {}}
|
||||||
|
title="Snowball"
|
||||||
|
width={1000}
|
||||||
|
height={1000}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
leftIcon={<GithubIcon />}
|
||||||
|
variant="tertiary"
|
||||||
|
>
|
||||||
|
Connect to GitHub
|
||||||
|
</Button>
|
||||||
|
</OauthPopup>
|
||||||
|
<Button
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
leftIcon={<GitTeaIcon />}
|
||||||
|
variant="tertiary"
|
||||||
|
>
|
||||||
|
Connect to GitTea
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex">
|
|
||||||
<OauthPopup
|
{/* TODO: Add ConnectAccountTabPanel */}
|
||||||
url={GITHUB_OAUTH_URL}
|
{/* <div className="rounded-l shadow p-2 flex-col justify-start items-start gap-2 inline-flex">
|
||||||
onCode={handleCode}
|
<ConnectAccountTabPanel />
|
||||||
onClose={() => {}}
|
</div> */}
|
||||||
title="Snowball"
|
|
||||||
width={1000}
|
|
||||||
height={1000}
|
|
||||||
>
|
|
||||||
<Button className="rounded-full mx-2">Connect to Github</Button>
|
|
||||||
</OauthPopup>
|
|
||||||
<Button className="rounded-full mx-2">Connect to Gitea</Button>
|
|
||||||
</div>
|
|
||||||
<ConnectAccountTabPanel />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Tabs, TabsHeader, Tab } from '@material-tailwind/react';
|
import { Tabs } from 'components/shared/Tabs';
|
||||||
|
|
||||||
const ConnectAccountTabPanel = () => {
|
const ConnectAccountTabPanel: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Tabs className="grid bg-white h-32 p-2 m-4 rounded-md" value="import">
|
<Tabs
|
||||||
<TabsHeader className="grid grid-cols-2">
|
defaultValue="Connect Accounts Tab Panel"
|
||||||
<Tab className="row-span-1" value="import">
|
orientation="horizontal"
|
||||||
Import a repository
|
className="mt-10"
|
||||||
</Tab>
|
>
|
||||||
<Tab className="row-span-2" value="template">
|
<Tabs.List>
|
||||||
Start with a template
|
{[
|
||||||
</Tab>
|
{ title: 'Import a repository' },
|
||||||
</TabsHeader>
|
{ title: 'Start with a template' },
|
||||||
{/* <TabsBody> */}
|
].map(({ title }, index) => (
|
||||||
{/* TODO: Add content */}
|
<Tabs.Trigger value={title} key={index}>
|
||||||
{/* </TabsBody> */}
|
{title}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
))}
|
||||||
|
</Tabs.List>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { Button, Typography } from '@material-tailwind/react';
|
import { Button, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
import { DeployStep, DeployStatus } from './DeployStep';
|
import { DeployStep, DeployStatus } from './DeployStep';
|
||||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||||
import ConfirmDialog from '../../shared/ConfirmDialog';
|
import ConfirmDialog from 'components/shared/ConfirmDialog';
|
||||||
|
|
||||||
|
const TIMEOUT_DURATION = 5000;
|
||||||
const Deploy = () => {
|
const Deploy = () => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const projectId = searchParams.get('projectId');
|
const projectId = searchParams.get('projectId');
|
||||||
@@ -21,6 +22,14 @@ const Deploy = () => {
|
|||||||
navigate(`/${orgSlug}/projects/create`);
|
navigate(`/${orgSlug}/projects/create`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timerID = setTimeout(() => {
|
||||||
|
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
||||||
|
}, TIMEOUT_DURATION);
|
||||||
|
|
||||||
|
return () => clearInterval(timerID);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between mb-6">
|
<div className="flex justify-between mb-6">
|
||||||
@@ -34,7 +43,12 @@ const Deploy = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={handleOpen} variant="outlined" size="sm">
|
<Button
|
||||||
|
onClick={handleOpen}
|
||||||
|
variant="outlined"
|
||||||
|
size="sm"
|
||||||
|
placeholder={''}
|
||||||
|
>
|
||||||
^ Cancel
|
^ Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +60,7 @@ const Deploy = () => {
|
|||||||
handleConfirm={handleCancel}
|
handleConfirm={handleCancel}
|
||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
<Typography variant="small">
|
<Typography variant="small" placeholder={''}>
|
||||||
This will halt the deployment and you will have to start the process
|
This will halt the deployment and you will have to start the process
|
||||||
from scratch.
|
from scratch.
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -74,14 +88,6 @@ const Deploy = () => {
|
|||||||
status={DeployStatus.NOT_STARTED}
|
status={DeployStatus.NOT_STARTED}
|
||||||
step="4"
|
step="4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
VIEW DEMO
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||