Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f7c6c73c9 | ||
|
|
2ac657c32e | ||
|
|
943d427db1 | ||
|
|
7e6b74208d | ||
|
|
2b78cab849 | ||
|
|
862862a9c5 | ||
|
|
6a3af491ca | ||
|
|
92f7d07f3f | ||
|
|
6fb94aeb11 | ||
|
|
1c848c5a89 | ||
|
|
17b5ff9a74 | ||
|
|
56adfc4806 | ||
|
|
b51d050ee6 | ||
|
|
8021ff7ff9 | ||
|
|
e850435c2d | ||
|
|
8a43698c43 | ||
|
|
499dba2c5e | ||
|
|
d498ba7e9a | ||
|
|
082353e9bc |
@@ -1,8 +1,8 @@
|
|||||||
# snowballtools-base
|
# snowballtools
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
- Clone the `snowballtools-base` repo
|
- Clone the `snowballtools` repo
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone git@github.com:snowball-tools/snowballtools-base.git
|
git clone git@github.com:snowball-tools/snowballtools-base.git
|
||||||
@@ -28,138 +28,63 @@
|
|||||||
cd packages/backend
|
cd packages/backend
|
||||||
```
|
```
|
||||||
|
|
||||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
- Load fixtures in database
|
||||||
- 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
|
|
||||||
|
|
||||||
### Backend Production
|
|
||||||
|
|
||||||
- Let us assume the following domains for backend and frontend
|
|
||||||
- Backend server: `api.snowballtools.com`
|
|
||||||
- Frontend app: `dashboard.snowballtools.com`
|
|
||||||
|
|
||||||
- Update the following in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[server]
|
|
||||||
...
|
|
||||||
[server.session]
|
|
||||||
# Secret should be changed to a different random string
|
|
||||||
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7sidnc382"
|
|
||||||
# Set URL of the frontend app
|
|
||||||
appOriginUrl = "https://dashboard.snowballtools.com"
|
|
||||||
# Set to true for session cookies to work behind proxy
|
|
||||||
trustProxy = true
|
|
||||||
# Set empty domain when using secure connection
|
|
||||||
domain = ""
|
|
||||||
```
|
|
||||||
|
|
||||||
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
```toml
|
|
||||||
...
|
|
||||||
[gitHub]
|
|
||||||
webhookUrl = "https://api.snowballtools.com"
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
```toml
|
|
||||||
...
|
|
||||||
[registryConfig]
|
|
||||||
fetchDeploymentRecordDelay = 5000
|
|
||||||
# Use actual port for REST endpoint
|
|
||||||
restEndpoint = "http://api.laconic.com:1317"
|
|
||||||
# Use actual port for GQL endpoint
|
|
||||||
gqlEndpoint = "http://api.laconic.com:9473/api"
|
|
||||||
# Set private key of account to be used in Laconicd
|
|
||||||
privateKey = "0wtu92cd4f1y791ezpjwgzzazni4dmd3q3mzqc3t6i6r9v06ji784tey6hwmnn69"
|
|
||||||
# Set Bond ID to be used for publishing records
|
|
||||||
bondId = "8xk8c2pb61kajwixpm223zvptr2x2ncajq0vd998p6aqhvqqep2reu6pik245epf"
|
|
||||||
chainId = "laconic_9000-1"
|
|
||||||
# Set authority that is existing in the chain
|
|
||||||
authority = "laconic"
|
|
||||||
[registryConfig.fee]
|
|
||||||
amount = "200000"
|
|
||||||
denom = "aphoton"
|
|
||||||
gas = "750000"
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
- Start the server in `packages/backend`
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn start
|
yarn db:load:fixtures
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backend Development
|
|
||||||
|
|
||||||
- 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 an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
- Client ID and secret will be available after creating Github 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
|
||||||
|
|
||||||
- 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)
|
||||||
- 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
|
- Get the private key and set `registryConfig.privateKey` 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
|
|
||||||
# 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
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
- 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)
|
||||||
# For registryConfig.restEndpoint
|
|
||||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
|
|
||||||
# 0.0.0.0:32777
|
|
||||||
|
|
||||||
# For registryConfig.gqlEndpoint
|
```bash
|
||||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
|
# For registryConfig.restEndpoint
|
||||||
# 0.0.0.0:32771
|
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
|
||||||
```
|
# 0.0.0.0:32777
|
||||||
|
|
||||||
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
|
# For registryConfig.gqlEndpoint
|
||||||
|
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
|
||||||
|
# 0.0.0.0:32771
|
||||||
|
```
|
||||||
|
|
||||||
- Run the script to create bond, reserve the authority and set authority bond
|
- Run the script to create bond, reserve the authority and set authority bond
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn test:registry:init
|
yarn registry:init
|
||||||
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
|
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
|
||||||
```
|
```
|
||||||
|
|
||||||
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
|
- 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
|
||||||
- [ngrok getting started](https://ngrok.com/docs/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]
|
||||||
@@ -170,7 +95,7 @@
|
|||||||
- Start the server in `packages/backend`
|
- Start the server in `packages/backend`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn start:dev
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Frontend
|
## Frontend
|
||||||
@@ -181,6 +106,12 @@
|
|||||||
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`
|
||||||
|
|
||||||
|
```env
|
||||||
|
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
|
||||||
|
```
|
||||||
|
|
||||||
- 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
|
||||||
|
|
||||||
```env
|
```env
|
||||||
@@ -190,32 +121,20 @@
|
|||||||
- Set `REACT_APP_GITHUB_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
- Set `REACT_APP_GITHUB_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# Set actual owner/name of the template repo that will be used for creating new repo
|
|
||||||
REACT_APP_GITHUB_TEMPLATE_REPO = cerc-io/test-progressive-web-app
|
REACT_APP_GITHUB_TEMPLATE_REPO = cerc-io/test-progressive-web-app
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frontend Production
|
### Development
|
||||||
|
|
||||||
- Let us assume the following domains for backend and frontend
|
- Start the React application
|
||||||
- Backend server: `api.snowballtools.com`
|
|
||||||
- Frontend app: `dashboard.snowballtools.com`
|
|
||||||
|
|
||||||
- Set the following values in [.env](packages/frontend/.env) file
|
```bash
|
||||||
|
yarn start
|
||||||
```env
|
|
||||||
# Backend server endpoint
|
|
||||||
REACT_APP_SERVER_URL = 'https://api.snowballtools.com'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
|
- The React application will be running in `http://localhost:3000/`
|
||||||
- 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
|
|
||||||
|
|
||||||
```env
|
### Production
|
||||||
REACT_APP_WALLET_CONNECT_ID = <PROJECT_ID>
|
|
||||||
```
|
|
||||||
|
|
||||||
- Build the React application
|
- Build the React application
|
||||||
|
|
||||||
@@ -229,28 +148,3 @@
|
|||||||
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/`
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
#!/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_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_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"
|
|
||||||
@@ -2,11 +2,6 @@
|
|||||||
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"
|
||||||
@@ -17,23 +12,13 @@
|
|||||||
clientId = ""
|
clientId = ""
|
||||||
clientSecret = ""
|
clientSecret = ""
|
||||||
|
|
||||||
[gitea]
|
|
||||||
[gitea.oAuth]
|
|
||||||
clientId = ""
|
|
||||||
clientSecret = ""
|
|
||||||
|
|
||||||
[registryConfig]
|
[registryConfig]
|
||||||
fetchDeploymentRecordDelay = 5000
|
|
||||||
restEndpoint = "http://localhost:1317"
|
restEndpoint = "http://localhost:1317"
|
||||||
gqlEndpoint = "http://localhost:9473/api"
|
gqlEndpoint = "http://localhost:9473/api"
|
||||||
chainId = "laconic_9000-1"
|
chainId = "laconic_9000-1"
|
||||||
privateKey = ""
|
privateKey = ""
|
||||||
bondId = ""
|
bondId = ""
|
||||||
authority = ""
|
|
||||||
[registryConfig.fee]
|
[registryConfig.fee]
|
||||||
amount = "200000"
|
amount = "200000"
|
||||||
denom = "aphoton"
|
denom = "aphoton"
|
||||||
gas = "750000"
|
gas = "550000"
|
||||||
|
|
||||||
[misc]
|
|
||||||
projectDomain = "apps.snowballtools.com"
|
|
||||||
|
|||||||
@@ -12,16 +12,13 @@
|
|||||||
"@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",
|
||||||
"nanoid": "3",
|
"nanoid": "3",
|
||||||
"nanoid-dictionary": "^5.0.0-beta.1",
|
"nanoid-dictionary": "^5.0.0-beta.1",
|
||||||
"node-fetch": "2",
|
|
||||||
"octokit": "^3.1.2",
|
"octokit": "^3.1.2",
|
||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
@@ -39,15 +36,12 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
|
"registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
|
||||||
"test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts",
|
"db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
|
||||||
"test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
|
"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",
|
||||||
"@types/node-fetch": "^2.6.11",
|
|
||||||
"@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",
|
||||||
"better-sqlite3": "^9.2.2",
|
"better-sqlite3": "^9.2.2",
|
||||||
|
|||||||
@@ -1,18 +1,7 @@
|
|||||||
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 {
|
||||||
@@ -33,8 +22,6 @@ export interface RegistryConfig {
|
|||||||
chainId: string;
|
chainId: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
bondId: string;
|
bondId: string;
|
||||||
fetchDeploymentRecordDelay: number;
|
|
||||||
authority: string;
|
|
||||||
fee: {
|
fee: {
|
||||||
amount: string;
|
amount: string;
|
||||||
denom: string;
|
denom: string;
|
||||||
@@ -42,14 +29,9 @@ export interface RegistryConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MiscConfig {
|
|
||||||
projectDomain: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
server: ServerConfig;
|
server: ServerConfig;
|
||||||
database: DatabaseConfig;
|
database: DatabaseConfig;
|
||||||
gitHub: GitHubConfig;
|
gitHub: GitHubConfig;
|
||||||
registryConfig: RegistryConfig;
|
registryConfig: RegistryConfig;
|
||||||
misc: MiscConfig;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import process from 'process';
|
export const DEFAULT_CONFIG_FILE_PATH = 'environments/local.toml';
|
||||||
|
|
||||||
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';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import assert from 'assert';
|
|||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
import { lowercase, numbers } from 'nanoid-dictionary';
|
import { lowercase, numbers } from 'nanoid-dictionary';
|
||||||
|
|
||||||
import { DatabaseConfig, MiscConfig } from './config';
|
import { DatabaseConfig } from './config';
|
||||||
import { User } from './entity/User';
|
import { User } from './entity/User';
|
||||||
import { Organization } from './entity/Organization';
|
import { Organization } from './entity/Organization';
|
||||||
import { Project } from './entity/Project';
|
import { Project } from './entity/Project';
|
||||||
@@ -13,10 +13,7 @@ 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 { getEntities, loadAndSaveData } from './utils';
|
import { PROJECT_DOMAIN } from './constants';
|
||||||
import { UserOrganization } from './entity/UserOrganization';
|
|
||||||
|
|
||||||
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
|
||||||
|
|
||||||
const log = debug('snowball:database');
|
const log = debug('snowball:database');
|
||||||
|
|
||||||
@@ -25,9 +22,8 @@ const nanoid = customAlphabet(lowercase + numbers, 8);
|
|||||||
// TODO: Fix order of methods
|
// TODO: Fix order of methods
|
||||||
export class Database {
|
export class Database {
|
||||||
private dataSource: DataSource;
|
private dataSource: DataSource;
|
||||||
private projectDomain: string;
|
|
||||||
|
|
||||||
constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) {
|
constructor ({ dbPath }: DatabaseConfig) {
|
||||||
this.dataSource = new DataSource({
|
this.dataSource = new DataSource({
|
||||||
type: 'better-sqlite3',
|
type: 'better-sqlite3',
|
||||||
database: dbPath,
|
database: dbPath,
|
||||||
@@ -35,21 +31,11 @@ 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> {
|
||||||
@@ -66,21 +52,14 @@ export class Database {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser (user: User, data: DeepPartial<User>): Promise<boolean> {
|
async updateUser (userId: string, data: DeepPartial<User>): Promise<boolean> {
|
||||||
const userRepository = this.dataSource.getRepository(User);
|
const userRepository = this.dataSource.getRepository(User);
|
||||||
const updateResult = await userRepository.update({ id: user.id }, data);
|
const updateResult = await userRepository.update({ id: userId }, data);
|
||||||
assert(updateResult.affected);
|
assert(updateResult.affected);
|
||||||
|
|
||||||
return updateResult.affected > 0;
|
return updateResult.affected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
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);
|
||||||
@@ -104,13 +83,6 @@ 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);
|
||||||
@@ -154,18 +126,10 @@ export class Database {
|
|||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get deployments with specified filter
|
|
||||||
*/
|
|
||||||
async getDeployments (options: FindManyOptions<Deployment>): Promise<Deployment[]> {
|
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
|
||||||
const deployments = await deploymentRepository.find(options);
|
|
||||||
|
|
||||||
return deployments;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
|
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
|
||||||
return this.getDeployments({
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
|
|
||||||
|
const deployments = await deploymentRepository.find({
|
||||||
relations: {
|
relations: {
|
||||||
project: true,
|
project: true,
|
||||||
domain: true,
|
domain: true,
|
||||||
@@ -180,6 +144,8 @@ export class Database {
|
|||||||
createdAt: 'DESC'
|
createdAt: 'DESC'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return deployments;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeployment (options: FindOneOptions<Deployment>): Promise<Deployment | null> {
|
async getDeployment (options: FindOneOptions<Deployment>): Promise<Deployment | null> {
|
||||||
@@ -196,14 +162,16 @@ export class Database {
|
|||||||
return domains;
|
return domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDeployment (data: DeepPartial<Deployment>): Promise<Deployment> {
|
async addDeployement (data: DeepPartial<Deployment>): Promise<Deployment> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
|
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
|
const url = `${data.project!.name}-${id}.${PROJECT_DOMAIN}`;
|
||||||
|
|
||||||
const updatedData = {
|
const updatedData = {
|
||||||
...data,
|
...data,
|
||||||
id
|
id,
|
||||||
|
url
|
||||||
};
|
};
|
||||||
const deployment = await deploymentRepository.save(updatedData);
|
const deployment = await deploymentRepository.save(updatedData);
|
||||||
|
|
||||||
@@ -344,20 +312,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentsByProjectIds (projectIds: string[], data: DeepPartial<Deployment>): Promise<boolean> {
|
async addProject (userId: string, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
|
||||||
|
|
||||||
const updateResult = await deploymentRepository
|
|
||||||
.createQueryBuilder()
|
|
||||||
.update(Deployment)
|
|
||||||
.set(data)
|
|
||||||
.where('projectId IN (:...projectIds)', { projectIds })
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return Boolean(updateResult.affected);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@@ -367,13 +322,15 @@ export class Database {
|
|||||||
// TODO: Set icon according to framework
|
// TODO: Set icon according to framework
|
||||||
newProject.icon = '';
|
newProject.icon = '';
|
||||||
|
|
||||||
newProject.owner = user;
|
newProject.owner = Object.assign(new User(), {
|
||||||
|
id: userId
|
||||||
|
});
|
||||||
|
|
||||||
newProject.organization = Object.assign(new Organization(), {
|
newProject.organization = Object.assign(new Organization(), {
|
||||||
id: organizationId
|
id: organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
|
newProject.subDomain = `${newProject.name}.${PROJECT_DOMAIN}`;
|
||||||
|
|
||||||
return projectRepository.save(newProject);
|
return projectRepository.save(newProject);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
import { Project } from './Project';
|
import { Project } from './Project';
|
||||||
import { Domain } from './Domain';
|
import { Domain } from './Domain';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
import { AppDeploymentRecordAttributes } from '../types';
|
|
||||||
|
|
||||||
export enum Environment {
|
export enum Environment {
|
||||||
Production = 'Production',
|
Production = 'Production',
|
||||||
@@ -26,19 +25,10 @@ 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
|
||||||
@@ -55,9 +45,6 @@ export class Deployment {
|
|||||||
@PrimaryColumn('varchar')
|
@PrimaryColumn('varchar')
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Column()
|
|
||||||
projectId!: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Project, { onDelete: 'CASCADE' })
|
@ManyToOne(() => Project, { onDelete: 'CASCADE' })
|
||||||
@JoinColumn({ name: 'projectId' })
|
@JoinColumn({ name: 'projectId' })
|
||||||
project!: Project;
|
project!: Project;
|
||||||
@@ -78,26 +65,14 @@ export class Deployment {
|
|||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
commitMessage!: string;
|
commitMessage!: string;
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
@Column('varchar')
|
||||||
url!: string | null;
|
url!: string;
|
||||||
|
|
||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
applicationRecordId!: string;
|
registryRecordId!: string;
|
||||||
|
|
||||||
@Column('simple-json')
|
@Column('simple-json')
|
||||||
applicationRecordData!: ApplicationRecord;
|
registryRecordData!: ApplicationRecord;
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
|
||||||
applicationDeploymentRequestId!: string | null;
|
|
||||||
|
|
||||||
@Column('simple-json', { nullable: true })
|
|
||||||
applicationDeploymentRequestData!: ApplicationDeploymentRequest | null;
|
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
|
||||||
applicationDeploymentRecordId!: string | null;
|
|
||||||
|
|
||||||
@Column('simple-json', { nullable: true })
|
|
||||||
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
|
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
enum: Environment
|
enum: Environment
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ 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')
|
||||||
@@ -43,6 +52,12 @@ export class Project {
|
|||||||
@Column('varchar', { length: 255, default: 'main' })
|
@Column('varchar', { length: 255, default: 'main' })
|
||||||
prodBranch!: string;
|
prodBranch!: string;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
registryRecordId!: string | null;
|
||||||
|
|
||||||
|
@Column('simple-json', { nullable: true })
|
||||||
|
registryRecordData!: ApplicationDeploymentRequest | null;
|
||||||
|
|
||||||
@Column('text', { default: '' })
|
@Column('text', { default: '' })
|
||||||
description!: string;
|
description!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -12,15 +12,10 @@ 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;
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
import { Organization } from './Organization';
|
import { Organization } from './Organization';
|
||||||
|
|
||||||
export enum Role {
|
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, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
const { server, database, gitHub, registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||||
|
|
||||||
const app = new OAuthApp({
|
const app = new OAuthApp({
|
||||||
clientType: OAUTH_CLIENT_TYPE,
|
clientType: OAUTH_CLIENT_TYPE,
|
||||||
@@ -27,11 +27,11 @@ export const main = async (): Promise<void> => {
|
|||||||
clientSecret: gitHub.oAuth.clientSecret
|
clientSecret: gitHub.oAuth.clientSecret
|
||||||
});
|
});
|
||||||
|
|
||||||
const db = new Database(database, misc);
|
const db = new Database(database);
|
||||||
await db.init();
|
await db.init();
|
||||||
|
|
||||||
const registry = new Registry(registryConfig);
|
const registry = new Registry(registryConfig);
|
||||||
const service = new Service({ gitHubConfig: gitHub, registryConfig }, db, app, registry);
|
const service = new Service({ gitHubConfig: gitHub }, 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);
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ 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 { ApplicationRecord, Deployment, ApplicationDeploymentRequest } from './entity/Deployment';
|
import { ApplicationDeploymentRequest } from './entity/Project';
|
||||||
import { AppDeploymentRecord, PackageJSON } from './types';
|
import { ApplicationRecord } from './entity/Deployment';
|
||||||
|
import { PackageJSON } from './types';
|
||||||
|
|
||||||
const log = debug('snowball:registry');
|
const log = debug('snowball:registry');
|
||||||
|
|
||||||
const APP_RECORD_TYPE = 'ApplicationRecord';
|
const APP_RECORD_TYPE = 'ApplicationRecord';
|
||||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
const DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRequest';
|
||||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
|
||||||
|
|
||||||
// TODO: Move registry code to laconic-sdk/watcher-ts
|
// TODO: Move registry code to laconic-sdk/watcher-ts
|
||||||
export class Registry {
|
export class Registry {
|
||||||
@@ -26,18 +26,16 @@ export class Registry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationRecord ({
|
async createApplicationRecord ({
|
||||||
appName,
|
|
||||||
packageJSON,
|
packageJSON,
|
||||||
commitHash,
|
commitHash,
|
||||||
appType,
|
appType,
|
||||||
repoUrl
|
repoUrl
|
||||||
}: {
|
}: {
|
||||||
appName: string,
|
|
||||||
packageJSON: PackageJSON
|
packageJSON: PackageJSON
|
||||||
commitHash: string,
|
commitHash: string,
|
||||||
appType: string,
|
appType: string,
|
||||||
repoUrl: string
|
repoUrl: string
|
||||||
}): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> {
|
}): Promise<{registryRecordId: string, registryRecordData: 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
|
||||||
@@ -60,7 +58,7 @@ export class Registry {
|
|||||||
repository_ref: commitHash,
|
repository_ref: commitHash,
|
||||||
repository: [repoUrl],
|
repository: [repoUrl],
|
||||||
app_type: appType,
|
app_type: appType,
|
||||||
name: appName,
|
...(packageJSON.name && { name: packageJSON.name }),
|
||||||
...(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 }),
|
||||||
@@ -81,25 +79,24 @@ 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(appName);
|
const crn = this.getCrn(packageJSON.name ?? '');
|
||||||
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({ cid: result.data.id, crn }, 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.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);
|
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 { registryRecordId: result.data.id, registryRecordData: applicationRecord };
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationDeploymentRequest (data: {
|
async createApplicationDeploymentRequest (data: {
|
||||||
deployment: Deployment,
|
|
||||||
appName: string,
|
appName: string,
|
||||||
packageJsonName: string,
|
commitHash: string,
|
||||||
repository: string,
|
repository: string,
|
||||||
environmentVariables: { [key: string]: string }
|
environmentVariables: { [key: string]: string }
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
applicationDeploymentRequestId: string,
|
registryRecordId: string,
|
||||||
applicationDeploymentRequestData: ApplicationDeploymentRequest
|
registryRecordData: 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]);
|
||||||
@@ -111,13 +108,13 @@ export class Registry {
|
|||||||
|
|
||||||
// Create record of type ApplicationDeploymentRequest and publish
|
// Create record of type ApplicationDeploymentRequest and publish
|
||||||
const applicationDeploymentRequest = {
|
const applicationDeploymentRequest = {
|
||||||
type: APP_DEPLOYMENT_REQUEST_TYPE,
|
type: DEPLOYMENT_RECORD_TYPE,
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
||||||
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
application: `${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
|
||||||
@@ -127,7 +124,7 @@ export class Registry {
|
|||||||
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.deployment.commitHash
|
repository_ref: data.commitHash
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,25 +140,17 @@ 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 { registryRecordId: result.data.id, registryRecordData: applicationDeploymentRequest };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getCrn (packageJsonName: string): string {
|
||||||
* Fetch ApplicationDeploymentRecords for deployments
|
const [arg1, arg2] = packageJsonName.split('/');
|
||||||
*/
|
|
||||||
async getDeploymentRecords (deployments: Deployment[]): Promise<AppDeploymentRecord[]> {
|
|
||||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
|
||||||
// TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute
|
|
||||||
const records = await this.registry.queryRecords({
|
|
||||||
type: APP_DEPLOYMENT_RECORD_TYPE
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
// Filter records with ApplicationRecord ids
|
if (arg2) {
|
||||||
return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application));
|
const authority = arg1.replace('@', '');
|
||||||
}
|
return `crn://${authority}/applications/${arg2}`;
|
||||||
|
}
|
||||||
|
|
||||||
getCrn (appName: string): string {
|
return `crn://${arg1}/applications/${arg1}`;
|
||||||
assert(this.registryConfig.authority, "Authority doesn't exist");
|
|
||||||
return `crn://${this.registryConfig.authority}/applications/${appName}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Permission } from './entity/ProjectMember';
|
|||||||
import { Domain } from './entity/Domain';
|
import { Domain } from './entity/Domain';
|
||||||
import { Project } from './entity/Project';
|
import { Project } from './entity/Project';
|
||||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||||
import { GitType } from './types';
|
|
||||||
|
|
||||||
const log = debug('snowball:resolver');
|
const log = debug('snowball:resolver');
|
||||||
|
|
||||||
@@ -15,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 context.user;
|
return service.getUser(context.userId);
|
||||||
},
|
},
|
||||||
|
|
||||||
organizations: async (_:any, __: any, context: any) => {
|
organizations: async (_:any, __: any, context: any) => {
|
||||||
return service.getOrganizationsByUserId(context.user);
|
return service.getOrganizationsByUserId(context.userId);
|
||||||
},
|
},
|
||||||
|
|
||||||
project: async (_: any, { projectId }: { projectId: string }) => {
|
project: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
@@ -27,11 +26,11 @@ 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.user, organizationSlug);
|
return service.getProjectsInOrganization(context.userId, organizationSlug);
|
||||||
},
|
},
|
||||||
|
|
||||||
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
return service.getDeploymentsByProjectId(projectId);
|
return service.getDeployementsByProjectId(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
environmentVariables: async (_: any, { projectId }: { projectId: string }) => {
|
environmentVariables: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
@@ -43,7 +42,7 @@ 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.user, searchText);
|
return service.searchProjects(context.userId, searchText);
|
||||||
},
|
},
|
||||||
|
|
||||||
domains: async (_:any, { projectId, filter }: { projectId: string, filter?: FindOptionsWhere<Domain> }) => {
|
domains: async (_:any, { projectId, filter }: { projectId: string, filter?: FindOptionsWhere<Domain> }) => {
|
||||||
@@ -55,7 +54,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
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.user, projectMemberId);
|
return await service.removeProjectMember(context.userId, projectMemberId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -120,7 +119,7 @@ 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.user, deploymentId));
|
return Boolean(await service.updateDeploymentToProd(context.userId, deploymentId));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -129,7 +128,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
|
|
||||||
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.user, organizationSlug, data);
|
return await service.addProject(context.userId, organizationSlug, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -147,7 +146,7 @@ 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.user, deploymentId));
|
return Boolean(await service.redeployToProd(context.userId, deploymentId));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -200,16 +199,7 @@ 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.user);
|
return await service.authenticateGitHub(code, context.userId);
|
||||||
} catch (err) {
|
|
||||||
log(err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
authenticateGit: async (_: any, { type, code }: { type: GitType, code: string }, context: any) => {
|
|
||||||
try {
|
|
||||||
return await service.authenticateGit(type, code, context.user);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -218,7 +208,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.user, { gitHubToken: null });
|
return service.unauthenticateGitHub(context.userId, { gitHubToken: null });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -26,11 +26,6 @@ enum DomainStatus {
|
|||||||
Pending
|
Pending
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GitType {
|
|
||||||
GitHub
|
|
||||||
Gitea
|
|
||||||
}
|
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
id: String!
|
id: String!
|
||||||
name: String
|
name: String
|
||||||
@@ -96,7 +91,7 @@ type Deployment {
|
|||||||
branch: String!
|
branch: String!
|
||||||
commitHash: String!
|
commitHash: String!
|
||||||
commitMessage: String!
|
commitMessage: String!
|
||||||
url: String
|
url: String!
|
||||||
environment: Environment!
|
environment: Environment!
|
||||||
isCurrent: Boolean!
|
isCurrent: Boolean!
|
||||||
status: DeploymentStatus!
|
status: DeploymentStatus!
|
||||||
@@ -208,6 +203,5 @@ type Mutation {
|
|||||||
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
|
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
|
||||||
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
|
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
|
||||||
authenticateGitHub(code: String!): AuthResult!
|
authenticateGitHub(code: String!): AuthResult!
|
||||||
authenticateGit(type: GitType!, code: String!): AuthResult!
|
|
||||||
unauthenticateGitHub: Boolean!
|
unauthenticateGitHub: Boolean!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,22 @@
|
|||||||
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 } from './constants';
|
import { DEFAULT_GQL_PATH, USER_ID } 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,
|
||||||
@@ -35,7 +24,6 @@ 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();
|
||||||
|
|
||||||
@@ -51,19 +39,9 @@ export const createAndStartServer = async (
|
|||||||
const server = new ApolloServer({
|
const server = new ApolloServer({
|
||||||
schema,
|
schema,
|
||||||
csrfPrevention: true,
|
csrfPrevention: true,
|
||||||
context: async ({ req }) => {
|
context: () => {
|
||||||
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
// TODO: Use userId derived from auth token
|
||||||
|
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
|
||||||
@@ -74,49 +52,13 @@ 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.use(express.json());
|
|
||||||
|
|
||||||
app.set('service', service);
|
app.set('service', service);
|
||||||
app.use('/auth', authRouter);
|
app.use(express.json());
|
||||||
app.use('/api/github', githubRouter);
|
app.use('/api/github', githubRouter);
|
||||||
|
|
||||||
httpServer.listen(port, host, () => {
|
httpServer.listen(port, host, () => {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import assert from 'assert';
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
||||||
import { Octokit, RequestError } from 'octokit';
|
import { Octokit, RequestError } from 'octokit';
|
||||||
import fetch from 'node-fetch';
|
|
||||||
|
|
||||||
import { OAuthApp } from '@octokit/oauth-app';
|
import { OAuthApp } from '@octokit/oauth-app';
|
||||||
|
|
||||||
@@ -15,18 +14,14 @@ import { Project } from './entity/Project';
|
|||||||
import { Permission, ProjectMember } from './entity/ProjectMember';
|
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 } from './config';
|
||||||
import { AppDeploymentRecord, GitPushEventPayload, GitType, PackageJSON } from './types';
|
import { GitPushEventPayload } 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';
|
||||||
const GITEA_ACCESS_TOKEN_ENDPOINT = 'https://git.vdb.to/login/oauth/access_token';
|
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
gitHubConfig: GitHubConfig
|
gitHubConfig: GitHubConfig
|
||||||
registryConfig: RegistryConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service {
|
export class Service {
|
||||||
@@ -35,112 +30,11 @@ export class Service {
|
|||||||
private registry: Registry;
|
private registry: Registry;
|
||||||
private config: Config;
|
private config: Config;
|
||||||
|
|
||||||
private deployRecordCheckTimeout?: NodeJS.Timeout;
|
|
||||||
|
|
||||||
constructor (config: Config, db: Database, app: OAuthApp, registry: Registry) {
|
constructor (config: Config, db: Database, app: OAuthApp, registry: Registry) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.oauthApp = app;
|
this.oauthApp = app;
|
||||||
this.registry = registry;
|
this.registry = registry;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize services
|
|
||||||
*/
|
|
||||||
init (): void {
|
|
||||||
// Start check for ApplicationDeploymentRecords asynchronously
|
|
||||||
this.checkDeployRecordsAndUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy services
|
|
||||||
*/
|
|
||||||
destroy (): void {
|
|
||||||
clearTimeout(this.deployRecordCheckTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks for ApplicationDeploymentRecord and update corresponding deployments
|
|
||||||
* Continues check in loop after a delay of DEPLOY_RECORD_CHECK_DELAY_MS
|
|
||||||
*/
|
|
||||||
async checkDeployRecordsAndUpdate (): Promise<void> {
|
|
||||||
// Fetch deployments in building state
|
|
||||||
const deployments = await this.db.getDeployments({
|
|
||||||
where: {
|
|
||||||
status: DeploymentStatus.Building
|
|
||||||
// TODO: Fetch and check records for recent deployments
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (deployments.length) {
|
|
||||||
log(`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`);
|
|
||||||
|
|
||||||
// Fetch ApplicationDeploymentRecord for deployments
|
|
||||||
const records = await this.registry.getDeploymentRecords(deployments);
|
|
||||||
log(`Found ${records.length} ApplicationDeploymentRecords`);
|
|
||||||
|
|
||||||
// Update deployments for which ApplicationDeploymentRecords were returned
|
|
||||||
if (records.length) {
|
|
||||||
await this.updateDeploymentsWithRecordData(records);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.deployRecordCheckTimeout = setTimeout(() => {
|
|
||||||
this.checkDeployRecordsAndUpdate();
|
|
||||||
}, this.config.registryConfig.fetchDeploymentRecordDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update deployments with ApplicationDeploymentRecord data
|
|
||||||
*/
|
|
||||||
async updateDeploymentsWithRecordData (records: AppDeploymentRecord[]): Promise<void> {
|
|
||||||
// Get deployments for ApplicationDeploymentRecords
|
|
||||||
const deployments = await this.db.getDeployments({
|
|
||||||
where: records.map(record => ({
|
|
||||||
applicationRecordId: record.attributes.application
|
|
||||||
})),
|
|
||||||
order: {
|
|
||||||
createdAt: 'DESC'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get project IDs of deployments that are in production environment
|
|
||||||
const productionDeploymentProjectIds = deployments.reduce((acc, deployment): Set<string> => {
|
|
||||||
if (deployment.environment === Environment.Production) {
|
|
||||||
acc.add(deployment.projectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, new Set<string>());
|
|
||||||
|
|
||||||
// Set old deployments isCurrent to false
|
|
||||||
await this.db.updateDeploymentsByProjectIds(Array.from(productionDeploymentProjectIds), { isCurrent: false });
|
|
||||||
|
|
||||||
const recordToDeploymentsMap = deployments.reduce((acc: {[key: string]: Deployment}, deployment) => {
|
|
||||||
acc[deployment.applicationRecordId] = deployment;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
// Update deployment data for ApplicationDeploymentRecords
|
|
||||||
const deploymentUpdatePromises = records.map(async (record) => {
|
|
||||||
const deployment = recordToDeploymentsMap[record.attributes.application];
|
|
||||||
|
|
||||||
await this.db.updateDeploymentById(
|
|
||||||
deployment.id,
|
|
||||||
{
|
|
||||||
applicationDeploymentRecordId: record.id,
|
|
||||||
applicationDeploymentRecordData: record.attributes,
|
|
||||||
url: record.attributes.url,
|
|
||||||
status: DeploymentStatus.Ready,
|
|
||||||
isCurrent: deployment.environment === Environment.Production
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
log(`Updated deployment ${deployment.id} with URL ${record.attributes.url}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(deploymentUpdatePromises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser (userId: string): Promise<User | null> {
|
async getUser (userId: string): Promise<User | null> {
|
||||||
@@ -151,36 +45,6 @@ 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');
|
||||||
@@ -188,8 +52,8 @@ export class Service {
|
|||||||
return new Octokit({ auth: user.gitHubToken });
|
return new Octokit({ auth: user.gitHubToken });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganizationsByUserId (user: User): Promise<Organization[]> {
|
async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
|
||||||
const dbOrganizations = await this.db.getOrganizationsByUserId(user.id);
|
const dbOrganizations = await this.db.getOrganizationsByUserId(userId);
|
||||||
return dbOrganizations;
|
return dbOrganizations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,12 +62,12 @@ export class Service {
|
|||||||
return dbProject;
|
return dbProject;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsInOrganization (user: User, organizationSlug: string): Promise<Project[]> {
|
async getProjectsInOrganization (userId:string, organizationSlug: string): Promise<Project[]> {
|
||||||
const dbProjects = await this.db.getProjectsInOrganization(user.id, organizationSlug);
|
const dbProjects = await this.db.getProjectsInOrganization(userId, organizationSlug);
|
||||||
return dbProjects;
|
return dbProjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
|
async getDeployementsByProjectId (projectId: string): Promise<Deployment[]> {
|
||||||
const dbDeployments = await this.db.getDeploymentsByProjectId(projectId);
|
const dbDeployments = await this.db.getDeploymentsByProjectId(projectId);
|
||||||
return dbDeployments;
|
return dbDeployments;
|
||||||
}
|
}
|
||||||
@@ -218,8 +82,8 @@ export class Service {
|
|||||||
return dbProjectMembers;
|
return dbProjectMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchProjects (user: User, searchText: string): Promise<Project[]> {
|
async searchProjects (userId: string, searchText: string): Promise<Project[]> {
|
||||||
const dbProjects = await this.db.getProjectsBySearchText(user.id, searchText);
|
const dbProjects = await this.db.getProjectsBySearchText(userId, searchText);
|
||||||
return dbProjects;
|
return dbProjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,17 +128,17 @@ export class Service {
|
|||||||
return newProjectMember;
|
return newProjectMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeProjectMember (user: User, projectMemberId: string): Promise<boolean> {
|
async removeProjectMember (userId: string, projectMemberId: string): Promise<boolean> {
|
||||||
const member = await this.db.getProjectMemberById(projectMemberId);
|
const member = await this.db.getProjectMemberById(projectMemberId);
|
||||||
|
|
||||||
if (String(member.member.id) === user.id) {
|
if (String(member.member.id) === userId) {
|
||||||
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(user.id) === String(memberProject.owner.id)) {
|
if (String(userId) === 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');
|
||||||
@@ -307,7 +171,7 @@ export class Service {
|
|||||||
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
return this.db.deleteEnvironmentVariable(environmentVariableId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentToProd (user: User, deploymentId: string): Promise<Deployment> {
|
async updateDeploymentToProd (userId: string, deploymentId: string): Promise<Deployment> {
|
||||||
const oldDeployment = await this.db.getDeployment({
|
const oldDeployment = await this.db.getDeployment({
|
||||||
where: { id: deploymentId },
|
where: { id: deploymentId },
|
||||||
relations: {
|
relations: {
|
||||||
@@ -321,12 +185,13 @@ export class Service {
|
|||||||
|
|
||||||
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(user.id);
|
const octokit = await this.getOctokit(userId);
|
||||||
|
|
||||||
const newDeployment = await this.createDeployment(user.id,
|
const newDeployement = await this.createDeployment(userId,
|
||||||
octokit,
|
octokit,
|
||||||
{
|
{
|
||||||
project: oldDeployment.project,
|
project: oldDeployment.project,
|
||||||
|
isCurrent: true,
|
||||||
branch: oldDeployment.branch,
|
branch: oldDeployment.branch,
|
||||||
environment: Environment.Production,
|
environment: Environment.Production,
|
||||||
domain: prodBranchDomains[0],
|
domain: prodBranchDomains[0],
|
||||||
@@ -334,7 +199,7 @@ export class Service {
|
|||||||
commitMessage: oldDeployment.commitMessage
|
commitMessage: oldDeployment.commitMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
return newDeployment;
|
return newDeployement;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createDeployment (
|
async createDeployment (
|
||||||
@@ -359,9 +224,7 @@ export class Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
||||||
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
|
const 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 });
|
||||||
@@ -369,64 +232,53 @@ export class Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 { registryRecordId, registryRecordData } = await this.registry.createApplicationRecord({
|
||||||
appName: repo,
|
|
||||||
packageJSON,
|
packageJSON,
|
||||||
appType: data.project!.template!,
|
appType: data.project!.template!,
|
||||||
commitHash: data.commitHash!,
|
commitHash: data.commitHash!,
|
||||||
repoUrl: recordData.repoUrl
|
repoUrl: recordData.repoUrl
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update previous deployment with prod branch domain
|
// Check if new deployment is set to current
|
||||||
// TODO: Fix unique constraint error for domain
|
if (data.isCurrent) {
|
||||||
if (data.domain) {
|
// Update previous current deployment
|
||||||
await this.db.updateDeployment({
|
await this.db.updateDeployment({
|
||||||
domainId: data.domain.id
|
project: {
|
||||||
}, {
|
id: data.project.id
|
||||||
domain: null
|
},
|
||||||
});
|
isCurrent: true
|
||||||
|
}, { isCurrent: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDeployment = await this.db.addDeployment({
|
// Update previous deployment with prod branch domain
|
||||||
|
// TODO: Fix unique constraint error for domain
|
||||||
|
await this.db.updateDeployment({
|
||||||
|
domainId: data.domain?.id
|
||||||
|
}, {
|
||||||
|
domain: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const newDeployement = await this.db.addDeployement({
|
||||||
project: data.project,
|
project: data.project,
|
||||||
branch: data.branch,
|
branch: data.branch,
|
||||||
commitHash: data.commitHash,
|
commitHash: data.commitHash,
|
||||||
commitMessage: data.commitMessage,
|
commitMessage: data.commitMessage,
|
||||||
environment: data.environment,
|
environment: data.environment,
|
||||||
|
isCurrent: data.isCurrent,
|
||||||
status: DeploymentStatus.Building,
|
status: DeploymentStatus.Building,
|
||||||
applicationRecordId,
|
registryRecordId,
|
||||||
applicationRecordData,
|
registryRecordData,
|
||||||
domain: data.domain,
|
domain: data.domain,
|
||||||
createdBy: Object.assign(new User(), {
|
createdBy: Object.assign(new User(), {
|
||||||
id: userId
|
id: userId
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`);
|
log(`Created deployment ${newDeployement.id} and published application record ${registryRecordId}`);
|
||||||
|
return newDeployement;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProject (user: User, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> {
|
async addProject (userId: string, 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
|
||||||
@@ -436,9 +288,9 @@ export class Service {
|
|||||||
throw new Error('Organization does not exist');
|
throw new Error('Organization does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = await this.db.addProject(user, organization.id, data);
|
const project = await this.db.addProject(userId, organization.id, data);
|
||||||
|
|
||||||
const octokit = await this.getOctokit(user.id);
|
const octokit = await this.getOctokit(userId);
|
||||||
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({
|
||||||
@@ -451,10 +303,11 @@ 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
|
||||||
await this.createDeployment(user.id,
|
const newDeployment = await this.createDeployment(userId,
|
||||||
octokit,
|
octokit,
|
||||||
{
|
{
|
||||||
project,
|
project,
|
||||||
|
isCurrent: true,
|
||||||
branch: project.prodBranch,
|
branch: project.prodBranch,
|
||||||
environment: Environment.Production,
|
environment: Environment.Production,
|
||||||
domain: null,
|
domain: null,
|
||||||
@@ -466,6 +319,27 @@ 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 { registryRecordId, registryRecordData } = await this.registry.createApplicationDeploymentRequest(
|
||||||
|
{
|
||||||
|
appName: newDeployment.registryRecordData.name!,
|
||||||
|
commitHash: latestCommit.sha,
|
||||||
|
repository: repoDetails.html_url,
|
||||||
|
environmentVariables: environmentVariablesObj
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.db.updateProjectById(project.id, {
|
||||||
|
registryRecordId,
|
||||||
|
registryRecordData
|
||||||
|
});
|
||||||
|
|
||||||
await this.createRepoHook(octokit, project);
|
await this.createRepoHook(octokit, project);
|
||||||
|
|
||||||
return project;
|
return project;
|
||||||
@@ -519,6 +393,7 @@ export class Service {
|
|||||||
octokit,
|
octokit,
|
||||||
{
|
{
|
||||||
project,
|
project,
|
||||||
|
isCurrent: project.prodBranch === branch,
|
||||||
branch,
|
branch,
|
||||||
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview,
|
environment: project.prodBranch === branch ? Environment.Production : Environment.Preview,
|
||||||
domain,
|
domain,
|
||||||
@@ -551,7 +426,7 @@ export class Service {
|
|||||||
return this.db.deleteDomainById(domainId);
|
return this.db.deleteDomainById(domainId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async redeployToProd (user: User, deploymentId: string): Promise<Deployment> {
|
async redeployToProd (userId: string, deploymentId: string): Promise<Deployment> {
|
||||||
const oldDeployment = await this.db.getDeployment({
|
const oldDeployment = await this.db.getDeployment({
|
||||||
relations: {
|
relations: {
|
||||||
project: true,
|
project: true,
|
||||||
@@ -567,21 +442,22 @@ export class Service {
|
|||||||
throw new Error('Deployment not found');
|
throw new Error('Deployment not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const octokit = await this.getOctokit(user.id);
|
const octokit = await this.getOctokit(userId);
|
||||||
|
|
||||||
const newDeployment = await this.createDeployment(user.id,
|
const newDeployement = await this.createDeployment(userId,
|
||||||
octokit,
|
octokit,
|
||||||
{
|
{
|
||||||
project: oldDeployment.project,
|
project: oldDeployment.project,
|
||||||
// TODO: Put isCurrent field in project
|
// TODO: Put isCurrent field in project
|
||||||
branch: oldDeployment.branch,
|
branch: oldDeployment.branch,
|
||||||
|
isCurrent: true,
|
||||||
environment: Environment.Production,
|
environment: Environment.Production,
|
||||||
domain: oldDeployment.domain,
|
domain: oldDeployment.domain,
|
||||||
commitHash: oldDeployment.commitHash,
|
commitHash: oldDeployment.commitHash,
|
||||||
commitMessage: oldDeployment.commitMessage
|
commitMessage: oldDeployment.commitMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
return newDeployment;
|
return newDeployement;
|
||||||
}
|
}
|
||||||
|
|
||||||
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> {
|
async rollbackDeployment (projectId: string, deploymentId: string): Promise<boolean> {
|
||||||
@@ -599,7 +475,7 @@ export class Service {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!oldCurrentDeployment) {
|
if (!oldCurrentDeployment) {
|
||||||
throw new Error('Current deployment doesnot exist');
|
throw new Error('Current deployement 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 });
|
||||||
@@ -693,59 +569,17 @@ export class Service {
|
|||||||
return updateResult;
|
return updateResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticateGitHub (code:string, user: User): Promise<{token: string}> {
|
async authenticateGitHub (code:string, userId: string): Promise<{token: string}> {
|
||||||
const { authentication: { token } } = await this.oauthApp.createToken({
|
const { authentication: { token } } = await this.oauthApp.createToken({
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.db.updateUser(user, { gitHubToken: token });
|
await this.db.updateUser(userId, { gitHubToken: token });
|
||||||
|
|
||||||
return { token };
|
return { token };
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticateGit (type: GitType, code:string, user: User): Promise<{token: string}> {
|
async unauthenticateGitHub (userId: string, data: DeepPartial<User>): Promise<boolean> {
|
||||||
let token: string;
|
return this.db.updateUser(userId, data);
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case GitType.GitHub:
|
|
||||||
({ authentication: { token } } = await this.oauthApp.createToken({
|
|
||||||
code
|
|
||||||
}));
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case GitType.Gitea: {
|
|
||||||
const response = await fetch(GITEA_ACCESS_TOKEN_ENDPOINT, {
|
|
||||||
method: 'post',
|
|
||||||
body: JSON.stringify({
|
|
||||||
// TODO: Fetch from config
|
|
||||||
client_id: '',
|
|
||||||
client_secret: '',
|
|
||||||
code,
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
// TODO: Get frontend app URL from config
|
|
||||||
redirect_uri: 'http://localhost:3000/organization/projects/create'
|
|
||||||
}),
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
|
|
||||||
assert(response.ok, `HTTP Error Response: ${response.status} ${response.statusText}`);
|
|
||||||
const data: any = await response.json();
|
|
||||||
({ access_token: token } = data);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: throw new Error(`Type ${type} not handled for Git authentication`);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(token, `Access token is not set for type ${type}`);
|
|
||||||
await this.db.updateUser(user, { gitHubToken: token });
|
|
||||||
|
|
||||||
return { token };
|
|
||||||
}
|
|
||||||
|
|
||||||
async unauthenticateGitHub (user: User, data: DeepPartial<User>): Promise<boolean> {
|
|
||||||
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;
|
||||||
@@ -25,32 +25,3 @@ export interface GitPushEventPayload {
|
|||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppDeploymentRecordAttributes {
|
|
||||||
application: string;
|
|
||||||
dns: string;
|
|
||||||
meta: string;
|
|
||||||
name: string;
|
|
||||||
request: string;
|
|
||||||
type: string;
|
|
||||||
url: string;
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RegistryRecord {
|
|
||||||
id: string;
|
|
||||||
names: string[] | null;
|
|
||||||
owners: string[];
|
|
||||||
bondId: string;
|
|
||||||
createTime: string;
|
|
||||||
expiryTime: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppDeploymentRecord extends RegistryRecord {
|
|
||||||
attributes: AppDeploymentRecordAttributes
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum GitType {
|
|
||||||
GitHub = 'GitHub',
|
|
||||||
Gitea = 'Gitea',
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ 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');
|
||||||
|
|
||||||
@@ -20,44 +19,3 @@ 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;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,13 +4,11 @@
|
|||||||
"domainIndex":0,
|
"domainIndex":0,
|
||||||
"createdByIndex": 0,
|
"createdByIndex": 0,
|
||||||
"id":"ffhae3zq",
|
"id":"ffhae3zq",
|
||||||
"status": "Ready",
|
"status": "Building",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": true,
|
"isCurrent": true,
|
||||||
"applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"registryRecordData": {},
|
||||||
"applicationDeploymentRequestId": "xqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -24,10 +22,8 @@
|
|||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Preview",
|
"environment": "Preview",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"registryRecordData": {},
|
||||||
"applicationDeploymentRequestId": "wqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -38,13 +34,11 @@
|
|||||||
"domainIndex":2,
|
"domainIndex":2,
|
||||||
"createdByIndex": 0,
|
"createdByIndex": 0,
|
||||||
"id":"qmgekyte",
|
"id":"qmgekyte",
|
||||||
"status": "Ready",
|
"status": "Error",
|
||||||
"environment": "Development",
|
"environment": "Development",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"registryRecordData": {},
|
||||||
"applicationDeploymentRequestId": "kqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -58,10 +52,8 @@
|
|||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"registryRecordData": {},
|
||||||
"applicationDeploymentRequestId": "yqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"branch": "prod",
|
"branch": "prod",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -72,13 +64,11 @@
|
|||||||
"domainIndex":3,
|
"domainIndex":3,
|
||||||
"createdByIndex": 1,
|
"createdByIndex": 1,
|
||||||
"id":"eO8cckxk",
|
"id":"eO8cckxk",
|
||||||
"status": "Ready",
|
"status": "Building",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": true,
|
"isCurrent": true,
|
||||||
"applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"registryRecordData": {},
|
||||||
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -92,10 +82,8 @@
|
|||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Preview",
|
"environment": "Preview",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"registryRecordData": {},
|
||||||
"applicationDeploymentRequestId": "tqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -106,13 +94,11 @@
|
|||||||
"domainIndex":5,
|
"domainIndex":5,
|
||||||
"createdByIndex": 1,
|
"createdByIndex": 1,
|
||||||
"id":"hwwr6sbx",
|
"id":"hwwr6sbx",
|
||||||
"status": "Ready",
|
"status": "Error",
|
||||||
"environment": "Development",
|
"environment": "Development",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
|
"registryRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi",
|
||||||
"applicationRecordData": {},
|
"registryRecordData": {},
|
||||||
"applicationDeploymentRequestId": "eqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -123,13 +109,11 @@
|
|||||||
"domainIndex":9,
|
"domainIndex":9,
|
||||||
"createdByIndex": 2,
|
"createdByIndex": 2,
|
||||||
"id":"ndxje48a",
|
"id":"ndxje48a",
|
||||||
"status": "Ready",
|
"status": "Building",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": true,
|
"isCurrent": true,
|
||||||
"applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"registryRecordData": {},
|
||||||
"applicationDeploymentRequestId": "dqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -143,10 +127,8 @@
|
|||||||
"status": "Ready",
|
"status": "Ready",
|
||||||
"environment": "Preview",
|
"environment": "Preview",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
"registryRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
"applicationRecordData": {},
|
"registryRecordData": {},
|
||||||
"applicationDeploymentRequestId": "aqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -157,13 +139,11 @@
|
|||||||
"domainIndex":8,
|
"domainIndex":8,
|
||||||
"createdByIndex": 2,
|
"createdByIndex": 2,
|
||||||
"id":"b4bpthjr",
|
"id":"b4bpthjr",
|
||||||
"status": "Ready",
|
"status": "Error",
|
||||||
"environment": "Development",
|
"environment": "Development",
|
||||||
"isCurrent": false,
|
"isCurrent": false,
|
||||||
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
"registryRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
||||||
"applicationRecordData": {},
|
"registryRecordData": {},
|
||||||
"applicationDeploymentRequestId": "uqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
@@ -174,13 +154,11 @@
|
|||||||
"domainIndex": 6,
|
"domainIndex": 6,
|
||||||
"createdByIndex": 2,
|
"createdByIndex": 2,
|
||||||
"id":"b4bpthjr",
|
"id":"b4bpthjr",
|
||||||
"status": "Ready",
|
"status": "Building",
|
||||||
"environment": "Production",
|
"environment": "Production",
|
||||||
"isCurrent": true,
|
"isCurrent": true,
|
||||||
"applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
"registryRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo",
|
||||||
"applicationRecordData": {},
|
"registryRecordData": {},
|
||||||
"applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
|
||||||
"applicationDeploymentRequestData": {},
|
|
||||||
"branch": "test",
|
"branch": "test",
|
||||||
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
"commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00",
|
||||||
"commitMessage": "subscription added",
|
"commitMessage": "subscription added",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
|
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
|
||||||
"name": "Laconic",
|
"name": "AirFoil",
|
||||||
"slug": "laconic-2"
|
"slug": "airfoil-2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"name": "example.snowballtools.xyz",
|
"name": "randomurl.snowballtools.xyz",
|
||||||
"status": "Live",
|
"status": "Live",
|
||||||
"branch": "main"
|
"branch": "main"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"name": "example.org",
|
"name": "saugatt.com",
|
||||||
"status": "Pending",
|
"status": "Pending",
|
||||||
"branch": "test"
|
"branch": "test"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 1,
|
"projectIndex": 1,
|
||||||
"name": "example.snowballtools.xyz",
|
"name": "randomurl.snowballtools.xyz",
|
||||||
"status": "Live",
|
"status": "Live",
|
||||||
"branch": "main"
|
"branch": "main"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 1,
|
"projectIndex": 1,
|
||||||
"name": "example.org",
|
"name": "saugatt.com",
|
||||||
"status": "Pending",
|
"status": "Pending",
|
||||||
"branch": "test"
|
"branch": "test"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"name": "example.snowballtools.xyz",
|
"name": "randomurl.snowballtools.xyz",
|
||||||
"status": "Live",
|
"status": "Live",
|
||||||
"branch": "main"
|
"branch": "main"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"name": "example.org",
|
"name": "saugatt.com",
|
||||||
"status": "Pending",
|
"status": "Pending",
|
||||||
"branch": "test"
|
"branch": "test"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"framework": "test",
|
"framework": "test",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
|
"registryRecordId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"registryRecordData": {},
|
||||||
"subDomain": "testProject.snowball.xyz"
|
"subDomain": "testProject.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23,6 +25,8 @@
|
|||||||
"framework": "test-2",
|
"framework": "test-2",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
|
"registryRecordId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"registryRecordData": {},
|
||||||
"subDomain": "testProject-2.snowball.xyz"
|
"subDomain": "testProject-2.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -36,6 +40,8 @@
|
|||||||
"framework": "test-3",
|
"framework": "test-3",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
|
"registryRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"registryRecordData": {},
|
||||||
"subDomain": "iglootools.snowball.xyz"
|
"subDomain": "iglootools.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -49,6 +55,8 @@
|
|||||||
"framework": "test-4",
|
"framework": "test-4",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
|
"registryRecordId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"registryRecordData": {},
|
||||||
"subDomain": "iglootools-2.snowball.xyz"
|
"subDomain": "iglootools-2.snowball.xyz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -62,6 +70,8 @@
|
|||||||
"framework": "test-5",
|
"framework": "test-5",
|
||||||
"webhooks": [],
|
"webhooks": [],
|
||||||
"icon": "",
|
"icon": "",
|
||||||
|
"registryRecordId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi",
|
||||||
|
"registryRecordData": {},
|
||||||
"subDomain": "snowball-2.snowball.xyz"
|
"subDomain": "snowball-2.snowball.xyz"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"projectIndex": 0,
|
"projectIndex": 0,
|
||||||
"name": "www.example.org",
|
"name": "www.saugatt.com",
|
||||||
"status": "Pending",
|
"status": "Pending",
|
||||||
"redirectToIndex": 1,
|
"redirectToIndex": 1,
|
||||||
"branch": "test"
|
"branch": "test"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 1,
|
"projectIndex": 1,
|
||||||
"name": "www.example.org",
|
"name": "www.saugatt.com",
|
||||||
"status": "Pending",
|
"status": "Pending",
|
||||||
"redirectToIndex": 3,
|
"redirectToIndex": 3,
|
||||||
"branch": "test"
|
"branch": "test"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"projectIndex": 2,
|
"projectIndex": 2,
|
||||||
"name": "www.example.org",
|
"name": "www.saugatt.com",
|
||||||
"status": "Pending",
|
"status": "Pending",
|
||||||
"redirectToIndex": 5,
|
"redirectToIndex": 5,
|
||||||
"branch": "test"
|
"branch": "test"
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
|
"id": "59f4355d-9549-4aac-9b54-eeefceeabef0",
|
||||||
"name": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
"name": "Saugat Yadav",
|
||||||
"email": "snowball@snowballtools.xyz",
|
"email": "saugaty@airfoil.studio",
|
||||||
"isVerified": true,
|
"isVerified": true
|
||||||
"ethAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "e505b212-8da6-48b2-9614-098225dab34b",
|
"id": "e505b212-8da6-48b2-9614-098225dab34b",
|
||||||
"name": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
|
"name": "Gideon Low",
|
||||||
"email": "alice@snowballtools.xyz",
|
"email": "gideonl@airfoil.studio",
|
||||||
"isVerified": true,
|
"isVerified": true
|
||||||
"ethAddress": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cd892fad-9138-4aa2-a62c-414a32776ea7",
|
"id": "cd892fad-9138-4aa2-a62c-414a32776ea7",
|
||||||
"name": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a",
|
"name": "Sushan Yadav",
|
||||||
"email": "bob@snowballtools.xyz",
|
"email": "sushany@airfoil.studio",
|
||||||
"isVerified": true,
|
"isVerified": true
|
||||||
"ethAddress": "0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ 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 { checkFileExists, getConfig, getEntities, loadAndSaveData } from '../src/utils';
|
import { getConfig } 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';
|
||||||
|
|
||||||
@@ -19,27 +20,50 @@ const log = debug('snowball:initialize-database');
|
|||||||
const USER_DATA_PATH = './fixtures/users.json';
|
const USER_DATA_PATH = './fixtures/users.json';
|
||||||
const PROJECT_DATA_PATH = './fixtures/projects.json';
|
const PROJECT_DATA_PATH = './fixtures/projects.json';
|
||||||
const ORGANIZATION_DATA_PATH = './fixtures/organizations.json';
|
const ORGANIZATION_DATA_PATH = './fixtures/organizations.json';
|
||||||
const USER_ORGANIZATION_DATA_PATH = './fixtures/user-organizations.json';
|
const USER_ORGANIZATION_DATA_PATH = './fixtures/user-orgnizations.json';
|
||||||
const PROJECT_MEMBER_DATA_PATH = './fixtures/project-members.json';
|
const PROJECT_MEMBER_DATA_PATH = './fixtures/project-members.json';
|
||||||
const PRIMARY_DOMAIN_DATA_PATH = './fixtures/primary-domains.json';
|
const PRIMARY_DOMAIN_DATA_PATH = './fixtures/primary-domains.json';
|
||||||
const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json';
|
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 generateTestData = async (dataSource: DataSource) => {
|
const loadAndSaveData = async <Entity extends ObjectLiteral>(entityType: EntityTarget<Entity>, dataSource: DataSource, filePath: string, relations?: any | undefined) => {
|
||||||
const userEntities = await getEntities(path.resolve(__dirname, USER_DATA_PATH));
|
const entitiesData = await fs.readFile(filePath, 'utf-8');
|
||||||
const savedUsers = await loadAndSaveData(User, dataSource, userEntities);
|
const entities = JSON.parse(entitiesData);
|
||||||
|
const entityRepository = dataSource.getRepository(entityType);
|
||||||
|
|
||||||
const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
const savedEntity:Entity[] = [];
|
||||||
const savedOrgs = await loadAndSaveData(Organization, dataSource, orgEntities);
|
|
||||||
|
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 savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH));
|
||||||
|
const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH));
|
||||||
|
|
||||||
const projectRelations = {
|
const projectRelations = {
|
||||||
owner: savedUsers,
|
owner: savedUsers,
|
||||||
organization: savedOrgs
|
organization: savedOrgs
|
||||||
};
|
};
|
||||||
|
|
||||||
const projectEntities = await getEntities(path.resolve(__dirname, PROJECT_DATA_PATH));
|
const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations);
|
||||||
const savedProjects = await loadAndSaveData(Project, dataSource, projectEntities, projectRelations);
|
|
||||||
|
|
||||||
const domainRepository = dataSource.getRepository(Domain);
|
const domainRepository = dataSource.getRepository(Domain);
|
||||||
|
|
||||||
@@ -47,16 +71,14 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
const primaryDomainsEntities = await getEntities(path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH));
|
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH), domainPrimaryRelations);
|
||||||
const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, primaryDomainsEntities, domainPrimaryRelations);
|
|
||||||
|
|
||||||
const domainRedirectedRelations = {
|
const domainRedirectedRelations = {
|
||||||
project: savedProjects,
|
project: savedProjects,
|
||||||
redirectTo: savedPrimaryDomains
|
redirectTo: savedPrimaryDomains
|
||||||
};
|
};
|
||||||
|
|
||||||
const redirectDomainsEntities = await getEntities(path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH));
|
await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH), domainRedirectedRelations);
|
||||||
await loadAndSaveData(Domain, dataSource, redirectDomainsEntities, domainRedirectedRelations);
|
|
||||||
|
|
||||||
const savedDomains = await domainRepository.find();
|
const savedDomains = await domainRepository.find();
|
||||||
|
|
||||||
@@ -65,16 +87,14 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
organization: savedOrgs
|
organization: savedOrgs
|
||||||
};
|
};
|
||||||
|
|
||||||
const userOrganizationsEntities = await getEntities(path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH));
|
await loadAndSaveData(UserOrganization, dataSource, path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), userOrganizationRelations);
|
||||||
await loadAndSaveData(UserOrganization, dataSource, userOrganizationsEntities, userOrganizationRelations);
|
|
||||||
|
|
||||||
const projectMemberRelations = {
|
const projectMemberRelations = {
|
||||||
member: savedUsers,
|
member: savedUsers,
|
||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
const projectMembersEntities = await getEntities(path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH));
|
await loadAndSaveData(ProjectMember, dataSource, path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH), projectMemberRelations);
|
||||||
await loadAndSaveData(ProjectMember, dataSource, projectMembersEntities, projectMemberRelations);
|
|
||||||
|
|
||||||
const deploymentRelations = {
|
const deploymentRelations = {
|
||||||
project: savedProjects,
|
project: savedProjects,
|
||||||
@@ -82,15 +102,23 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
createdBy: savedUsers
|
createdBy: savedUsers
|
||||||
};
|
};
|
||||||
|
|
||||||
const deploymentsEntities = await getEntities(path.resolve(__dirname, DEPLOYMENT_DATA_PATH));
|
await loadAndSaveData(Deployment, dataSource, path.resolve(__dirname, DEPLOYMENT_DATA_PATH), deploymentRelations);
|
||||||
await loadAndSaveData(Deployment, dataSource, deploymentsEntities, deploymentRelations);
|
|
||||||
|
|
||||||
const environmentVariableRelations = {
|
const environmentVariableRelations = {
|
||||||
project: savedProjects
|
project: savedProjects
|
||||||
};
|
};
|
||||||
|
|
||||||
const environmentVariablesEntities = await getEntities(path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH));
|
await loadAndSaveData(EnvironmentVariable, dataSource, path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH), environmentVariableRelations);
|
||||||
await loadAndSaveData(EnvironmentVariable, dataSource, environmentVariablesEntities, environmentVariableRelations);
|
};
|
||||||
|
|
||||||
|
const checkFileExists = async (filePath: string) => {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath, fs.constants.F_OK);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
log(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
|
|||||||
@@ -11,19 +11,19 @@ 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 authorityNames) {
|
for await (const name of AUTHORITY_NAMES) {
|
||||||
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);
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
import debug from 'debug';
|
|
||||||
import { DataSource } from 'typeorm';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import { Registry } from '@cerc-io/laconic-sdk';
|
|
||||||
|
|
||||||
import { Config } from '../src/config';
|
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
|
||||||
import { getConfig } from '../src/utils';
|
|
||||||
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
|
||||||
|
|
||||||
const log = debug('snowball:publish-deploy-records');
|
|
||||||
|
|
||||||
async function main () {
|
|
||||||
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
|
||||||
|
|
||||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
|
||||||
|
|
||||||
const dataSource = new DataSource({
|
|
||||||
type: 'better-sqlite3',
|
|
||||||
database: database.dbPath,
|
|
||||||
synchronize: true,
|
|
||||||
entities: [path.join(__dirname, '../src/entity/*')]
|
|
||||||
});
|
|
||||||
|
|
||||||
await dataSource.initialize();
|
|
||||||
|
|
||||||
const deploymentRepository = dataSource.getRepository(Deployment);
|
|
||||||
const deployments = await deploymentRepository.find({
|
|
||||||
relations: {
|
|
||||||
project: true
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
status: DeploymentStatus.Building
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for await (const deployment of deployments) {
|
|
||||||
const url = `${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
|
|
||||||
|
|
||||||
const applicationDeploymentRecord = {
|
|
||||||
type: 'ApplicationDeploymentRecord',
|
|
||||||
version: '0.0.1',
|
|
||||||
name: deployment.applicationRecordData.name,
|
|
||||||
application: deployment.applicationRecordId,
|
|
||||||
|
|
||||||
// TODO: Create DNS record
|
|
||||||
dns: 'bafyreihlymqggsgqiqawvehkpr2imt4l3u6q7um7xzjrux5rhsvwnuyewm',
|
|
||||||
|
|
||||||
// Using dummy values
|
|
||||||
meta: JSON.stringify({
|
|
||||||
config: 'da39a3ee5e6b4b0d3255bfef95601890afd80709',
|
|
||||||
so: '66fcfa49a1664d4cb4ce4f72c1c0e151'
|
|
||||||
}),
|
|
||||||
|
|
||||||
request: deployment.applicationDeploymentRequestId,
|
|
||||||
url
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await registry.setRecord(
|
|
||||||
{
|
|
||||||
privateKey: registryConfig.privateKey,
|
|
||||||
record: applicationDeploymentRecord,
|
|
||||||
bondId: registryConfig.bondId
|
|
||||||
},
|
|
||||||
'',
|
|
||||||
registryConfig.fee
|
|
||||||
);
|
|
||||||
|
|
||||||
log('Application deployment record data:', applicationDeploymentRecord);
|
|
||||||
log(`Application deployment record published: ${result.data.id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
log(err);
|
|
||||||
});
|
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
|
||||||
|
|
||||||
REACT_APP_GITHUB_CLIENT_ID =
|
REACT_APP_GITHUB_CLIENT_ID =
|
||||||
REACT_APP_GITHUB_TEMPLATE_REPO =
|
REACT_APP_GITHUB_TEMPLATE_REPO =
|
||||||
|
|
||||||
REACT_APP_GITEA_CLIENT_ID =
|
|
||||||
|
|
||||||
REACT_APP_WALLET_CONNECT_ID =
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
// 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\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -3,8 +3,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/inter": "^5.0.16",
|
||||||
"@material-tailwind/react": "^2.1.7",
|
"@material-tailwind/react": "^2.1.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",
|
||||||
@@ -12,10 +12,7 @@
|
|||||||
"@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",
|
|
||||||
"date-fns": "^3.0.1",
|
"date-fns": "^3.0.1",
|
||||||
"downshift": "^8.2.3",
|
"downshift": "^8.2.3",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
@@ -33,12 +30,10 @@
|
|||||||
"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",
|
||||||
"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": {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square150x150logo src="/mstile-150x150.png"/>
|
|
||||||
<TileColor>#2d89ef</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
||||||
|
Before Width: | Height: | Size: 674 B |
|
Before Width: | Height: | Size: 989 B |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 3.8 KiB |
@@ -2,23 +2,42 @@
|
|||||||
<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
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="snowball tools dashboard"
|
content="Web site created using create-react-app"
|
||||||
/>
|
/>
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png">
|
<!--
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png">
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
<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>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,8 @@ 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';
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -44,14 +44,14 @@ const router = createBrowserRouter([
|
|||||||
path: '/',
|
path: '/',
|
||||||
element: <Index />,
|
element: <Index />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
element: <Login />,
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <RouterProvider router={router} />;
|
return (
|
||||||
|
<OctokitProvider>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</OctokitProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"projectid": 1,
|
"projectid": 1,
|
||||||
"name": "example.snowballtools.xyz",
|
"name": "randomurl.snowballtools.xyz",
|
||||||
"status": "live",
|
"status": "live",
|
||||||
"record": null,
|
"record": null,
|
||||||
"isRedirectedto": false
|
"isRedirectedto": false
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"projectid": 1,
|
"projectid": 1,
|
||||||
"name": "example.org",
|
"name": "saugatt.com",
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"record": {
|
"record": {
|
||||||
"type": "A",
|
"type": "A",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"projectid": 1,
|
"projectid": 1,
|
||||||
"name": "www.example.org",
|
"name": "www.saugatt.com",
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"record": {
|
"record": {
|
||||||
"type": "CNAME",
|
"type": "CNAME",
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Saugat Yadav",
|
||||||
|
"email": "saugaty@airfoil.studio",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gideon Low",
|
||||||
|
"email": "gideonl@airfoil.studio",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sushan Yadav",
|
||||||
|
"email": "sushany@airfoil.studio",
|
||||||
|
"id": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
"[20:50:03.502] Running build in Washington, D.C., USA (East) – iad1",
|
"[20:50:03.502] Running build in Washington, D.C., USA (East) – iad1",
|
||||||
"[20:50:03.641] Cloning github.com/cerc-io/nextjs2 (Branch: main, Commit: 4a5f47f)",
|
"[20:50:03.641] Cloning github.com/saugatyadav11/nextjs2 (Branch: main, Commit: 4a5f47f)",
|
||||||
"[20:50:04.004] Previous build cache not available",
|
"[20:50:04.004] Previous build cache not available",
|
||||||
"[20:50:04.118] Cloning completed: 480.574ms",
|
"[20:50:04.118] Cloning completed: 480.574ms",
|
||||||
"[20:50:04.382] Running 'vercel build'",
|
"[20:50:04.382] Running 'vercel build'",
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"icon": "^",
|
||||||
|
"name": "iglootools",
|
||||||
|
"title": "Iglootools",
|
||||||
|
"domain": null,
|
||||||
|
"organization": "Airfoil",
|
||||||
|
"url": "iglootools.co",
|
||||||
|
"createdAt": "2023-12-07T04:20:00",
|
||||||
|
"createdBy": "Alice",
|
||||||
|
"deployment": "iglootools.snowballtools.co",
|
||||||
|
"source": "feature/add-remote-control",
|
||||||
|
"latestCommit": {
|
||||||
|
"message": "subscription added",
|
||||||
|
"createdAt": "2023-12-11T04:20:00",
|
||||||
|
"branch": "main"
|
||||||
|
},
|
||||||
|
"repositoryId": 1,
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"permissions": ["view", "edit"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"permissions": ["view"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ownerId": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"icon": "^",
|
||||||
|
"name": "snowball-starter-kit",
|
||||||
|
"title": "Snowball Starter Kit",
|
||||||
|
"domain": null,
|
||||||
|
"organization": "Snowball",
|
||||||
|
"url": "starterkit.snowballtools.com",
|
||||||
|
"createdAt": "2023-12-04T04:20:00",
|
||||||
|
"createdBy": "Bob",
|
||||||
|
"deployment": "deploy.snowballtools.com",
|
||||||
|
"source": "prod/add-docker-compose",
|
||||||
|
"latestCommit": {
|
||||||
|
"message": "component updates",
|
||||||
|
"createdAt": "2023-12-11T04:20:00",
|
||||||
|
"branch": "staging"
|
||||||
|
},
|
||||||
|
"repositoryId": 1,
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"permissions": ["view"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ownerId": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"icon": "^",
|
||||||
|
"name": "web3-android",
|
||||||
|
"title": "Web3 Android",
|
||||||
|
"domain": null,
|
||||||
|
"organization": "Personal",
|
||||||
|
"url": "web3fordroids.com",
|
||||||
|
"createdAt": "2023-12-01T04:20:00",
|
||||||
|
"createdBy": "Charlie",
|
||||||
|
"deployment": "deploy.web3fordroids.com",
|
||||||
|
"source": "dev/style-page",
|
||||||
|
"latestCommit": {
|
||||||
|
"message": "No repo connected",
|
||||||
|
"createdAt": "2023-12-01T04:20:00",
|
||||||
|
"branch": "main"
|
||||||
|
},
|
||||||
|
"repositoryId": 1,
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"permissions": ["view", "edit"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"permissions": ["view"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ownerId": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"icon": "^",
|
||||||
|
"name": "passkeys-demo",
|
||||||
|
"title": "Passkeys Demo",
|
||||||
|
"domain": null,
|
||||||
|
"organization": "Airfoil",
|
||||||
|
"url": "passkeys.iglootools.xyz",
|
||||||
|
"createdAt": "2023-12-01T04:20:00",
|
||||||
|
"createdBy": "David",
|
||||||
|
"deployment": "demo.passkeys.xyz",
|
||||||
|
"source": "dev/style-page",
|
||||||
|
"latestCommit": {
|
||||||
|
"message": "hello world",
|
||||||
|
"createdAt": "2023-12-01T04:20:00",
|
||||||
|
"branch": "main"
|
||||||
|
},
|
||||||
|
"repositoryId": 1,
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"permissions": ["view", "edit"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"permissions": ["view"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ownerId": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"icon": "^",
|
||||||
|
"name": "iglootools",
|
||||||
|
"title": "Iglootools",
|
||||||
|
"domain": null,
|
||||||
|
"organization": "Airfoil",
|
||||||
|
"url": "iglootools.xyz",
|
||||||
|
"createdAt": "2023-12-11T04:20:00",
|
||||||
|
"createdBy": "Erin",
|
||||||
|
"deployment": "staging.snowballtools.com",
|
||||||
|
"source": "prod/fix-error",
|
||||||
|
"latestCommit": {
|
||||||
|
"message": "404 added",
|
||||||
|
"createdAt": "2023-12-09T04:20:00",
|
||||||
|
"branch": "main"
|
||||||
|
},
|
||||||
|
"repositoryId": 1,
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"permissions": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ownerId": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"icon": "^",
|
||||||
|
"name": "iglootools",
|
||||||
|
"title": "Iglootools",
|
||||||
|
"domain": null,
|
||||||
|
"organization": "Airfoil",
|
||||||
|
"url": "iglootools.xyz",
|
||||||
|
"createdAt": "2023-12-11T04:20:00",
|
||||||
|
"createdBy": "Frank",
|
||||||
|
"deployment": "iglootools.snowballtools.com",
|
||||||
|
"source": "prod/fix-error",
|
||||||
|
"latestCommit": {
|
||||||
|
"message": "design system integrated",
|
||||||
|
"createdAt": "2023-12-09T04:20:00",
|
||||||
|
"branch": "prod"
|
||||||
|
},
|
||||||
|
"repositoryId": 1,
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"permissions": ["view"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ownerId": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -3,7 +3,6 @@ import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
|
|||||||
import { Organization } from 'gql-client';
|
import { Organization } from 'gql-client';
|
||||||
|
|
||||||
import { Typography, Option } from '@material-tailwind/react';
|
import { Typography, Option } from '@material-tailwind/react';
|
||||||
import { useDisconnect } from 'wagmi';
|
|
||||||
|
|
||||||
import { useGQLClient } from '../context/GQLClientContext';
|
import { useGQLClient } from '../context/GQLClientContext';
|
||||||
import AsyncSelect from './shared/AsyncSelect';
|
import AsyncSelect from './shared/AsyncSelect';
|
||||||
@@ -12,7 +11,6 @@ const Sidebar = () => {
|
|||||||
const { orgSlug } = useParams();
|
const { orgSlug } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
const { disconnect } = useDisconnect();
|
|
||||||
|
|
||||||
const [selectedOrgSlug, setSelectedOrgSlug] = useState(orgSlug);
|
const [selectedOrgSlug, setSelectedOrgSlug] = useState(orgSlug);
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
@@ -27,11 +25,6 @@ const Sidebar = () => {
|
|||||||
setSelectedOrgSlug(orgSlug);
|
setSelectedOrgSlug(orgSlug);
|
||||||
}, [orgSlug]);
|
}, [orgSlug]);
|
||||||
|
|
||||||
const handleLogOut = useCallback(() => {
|
|
||||||
disconnect();
|
|
||||||
navigate('/login');
|
|
||||||
}, [disconnect, navigate]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full p-4">
|
<div className="flex flex-col h-full p-4">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
@@ -83,11 +76,8 @@ const Sidebar = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grow flex flex-col justify-end">
|
<div className="grow flex flex-col justify-end">
|
||||||
<a className="cursor-pointer" onClick={handleLogOut}>
|
<div>Documentation</div>
|
||||||
Log Out
|
<div>Support</div>
|
||||||
</a>
|
|
||||||
<a className="cursor-pointer">Documentation</a>
|
|
||||||
<a className="cursor-pointer">Support</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
|
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 { GitType } from 'gql-client';
|
|
||||||
|
|
||||||
import { Button } from '@material-tailwind/react';
|
|
||||||
|
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
import ConnectAccountTabPanel from './ConnectAccountTabPanel';
|
import ConnectAccountTabPanel from './ConnectAccountTabPanel';
|
||||||
|
|
||||||
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=${
|
||||||
process.env.REACT_APP_GITHUB_CLIENT_ID
|
process.env.REACT_APP_GITHUB_CLIENT_ID
|
||||||
}&scope=${encodeURIComponent(SCOPES)}`;
|
}&scope=${encodeURIComponent(SCOPES)}`;
|
||||||
|
|
||||||
const REDIRECT_URI = `${window.location.origin}/organization/projects/create`;
|
|
||||||
const GITEA_OAUTH_URL = `https://git.vdb.to/login/oauth/authorize?client_id=${process.env.REACT_APP_GITEA_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code`;
|
|
||||||
|
|
||||||
interface ConnectAccountInterface {
|
interface ConnectAccountInterface {
|
||||||
onAuth: (token: string) => void;
|
onAuth: (token: string) => void;
|
||||||
}
|
}
|
||||||
@@ -23,13 +17,11 @@ interface ConnectAccountInterface {
|
|||||||
const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
|
|
||||||
const handleCode = async (type: GitType, 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 {
|
||||||
authenticateGit: { token },
|
authenticateGitHub: { token },
|
||||||
} = await client.authenticateGit(type, code);
|
} = await client.authenticateGitHub(code);
|
||||||
|
|
||||||
// TODO: Handle token according to Git type
|
|
||||||
onToken(token);
|
onToken(token);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,7 +40,7 @@ const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
|||||||
<div className="mt-2 flex">
|
<div className="mt-2 flex">
|
||||||
<OauthPopup
|
<OauthPopup
|
||||||
url={GITHUB_OAUTH_URL}
|
url={GITHUB_OAUTH_URL}
|
||||||
onCode={(code) => handleCode(GitType.GitHub, code)}
|
onCode={handleCode}
|
||||||
onClose={() => {}}
|
onClose={() => {}}
|
||||||
title="Snowball"
|
title="Snowball"
|
||||||
width={1000}
|
width={1000}
|
||||||
@@ -56,16 +48,7 @@ const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
|||||||
>
|
>
|
||||||
<Button className="rounded-full mx-2">Connect to Github</Button>
|
<Button className="rounded-full mx-2">Connect to Github</Button>
|
||||||
</OauthPopup>
|
</OauthPopup>
|
||||||
<OauthPopup
|
<Button className="rounded-full mx-2">Connect to Gitea</Button>
|
||||||
url={GITEA_OAUTH_URL}
|
|
||||||
onCode={(code) => handleCode(GitType.Gitea, code)}
|
|
||||||
onClose={() => {}}
|
|
||||||
title="Snowball"
|
|
||||||
width={1000}
|
|
||||||
height={1000}
|
|
||||||
>
|
|
||||||
<Button className="rounded-full mx-2">Connect to Gitea</Button>
|
|
||||||
</OauthPopup>
|
|
||||||
</div>
|
</div>
|
||||||
<ConnectAccountTabPanel />
|
<ConnectAccountTabPanel />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback } 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';
|
||||||
@@ -7,7 +7,6 @@ import { DeployStep, DeployStatus } from './DeployStep';
|
|||||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||||
import ConfirmDialog from '../../shared/ConfirmDialog';
|
import ConfirmDialog from '../../shared/ConfirmDialog';
|
||||||
|
|
||||||
const INTERVAL_DURATION = 5000;
|
|
||||||
const Deploy = () => {
|
const Deploy = () => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const projectId = searchParams.get('projectId');
|
const projectId = searchParams.get('projectId');
|
||||||
@@ -22,14 +21,6 @@ const Deploy = () => {
|
|||||||
navigate(`/${orgSlug}/projects/create`);
|
navigate(`/${orgSlug}/projects/create`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timerID = setTimeout(() => {
|
|
||||||
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
|
||||||
}, INTERVAL_DURATION);
|
|
||||||
|
|
||||||
return () => clearInterval(timerID);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between mb-6">
|
<div className="flex justify-between mb-6">
|
||||||
@@ -83,6 +74,14 @@ 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { Chip, IconButton, Spinner } from '@material-tailwind/react';
|
import { Chip, IconButton } from '@material-tailwind/react';
|
||||||
|
|
||||||
import { relativeTimeISO } from '../../../utils/time';
|
import { relativeTimeISO } from '../../../utils/time';
|
||||||
import { GitRepositoryDetails } from '../../../types';
|
import { GitRepositoryDetails } from '../../../types';
|
||||||
@@ -15,7 +14,6 @@ interface ProjectRepoCardProps {
|
|||||||
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
|
||||||
|
|
||||||
const { orgSlug } = useParams();
|
const { orgSlug } = useParams();
|
||||||
|
|
||||||
@@ -24,7 +22,6 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
const { addProject } = await client.addProject(orgSlug!, {
|
const { addProject } = await client.addProject(orgSlug!, {
|
||||||
name: `${repository.owner!.login}-${repository.name}`,
|
name: `${repository.owner!.login}-${repository.name}`,
|
||||||
prodBranch: repository.default_branch!,
|
prodBranch: repository.default_branch!,
|
||||||
@@ -33,13 +30,7 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
|||||||
template: 'webapp',
|
template: 'webapp',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Boolean(addProject)) {
|
navigate(`import?projectId=${addProject.id}`);
|
||||||
setIsLoading(false);
|
|
||||||
navigate(`import?projectId=${addProject.id}`);
|
|
||||||
} else {
|
|
||||||
setIsLoading(false);
|
|
||||||
toast.error('Failed to create project');
|
|
||||||
}
|
|
||||||
}, [client, repository]);
|
}, [client, repository]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -62,13 +53,9 @@ const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
|||||||
</div>
|
</div>
|
||||||
<p>{repository.updated_at && relativeTimeISO(repository.updated_at)}</p>
|
<p>{repository.updated_at && relativeTimeISO(repository.updated_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
{isLoading ? (
|
<div className="hidden group-hover:block">
|
||||||
<Spinner className="h-4 w-4" />
|
<IconButton size="sm">{'>'}</IconButton>
|
||||||
) : (
|
</div>
|
||||||
<div className="hidden group-hover:block">
|
|
||||||
<IconButton size="sm">{'>'}</IconButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ import { Octokit } from 'octokit';
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { useDebounce } from 'usehooks-ts';
|
import { useDebounce } from 'usehooks-ts';
|
||||||
|
|
||||||
import { Button, Typography, Option } from '@material-tailwind/react';
|
import { Button, Typography, Option, Select } from '@material-tailwind/react';
|
||||||
|
|
||||||
import SearchBar from '../../SearchBar';
|
import SearchBar from '../../SearchBar';
|
||||||
import ProjectRepoCard from './ProjectRepoCard';
|
import ProjectRepoCard from './ProjectRepoCard';
|
||||||
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
|
import { GitOrgDetails, GitRepositoryDetails } from '../../../types';
|
||||||
import AsyncSelect from '../../shared/AsyncSelect';
|
|
||||||
|
|
||||||
const DEFAULT_SEARCHED_REPO = '';
|
const DEFAULT_SEARCHED_REPO = '';
|
||||||
const REPOS_PER_PAGE = 5;
|
const REPOS_PER_PAGE = 5;
|
||||||
@@ -110,7 +109,8 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex gap-2 mb-2">
|
<div className="flex gap-2 mb-2">
|
||||||
<div className="basis-1/3">
|
<div className="basis-1/3">
|
||||||
<AsyncSelect
|
{/* TODO: Fix selection of Git user at start */}
|
||||||
|
<Select
|
||||||
value={selectedAccount}
|
value={selectedAccount}
|
||||||
onChange={(value) => setSelectedAccount(value!)}
|
onChange={(value) => setSelectedAccount(value!)}
|
||||||
>
|
>
|
||||||
@@ -119,7 +119,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
|||||||
^ {account.login}
|
^ {account.login}
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</AsyncSelect>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="basis-2/3">
|
<div className="basis-2/3">
|
||||||
<SearchBar
|
<SearchBar
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Typography, IconButton, Avatar } from '@material-tailwind/react';
|
import { Typography, IconButton } from '@material-tailwind/react';
|
||||||
|
|
||||||
import { relativeTimeISO } from '../../../utils/time';
|
import { relativeTimeISO } from '../../../utils/time';
|
||||||
import { GitCommitWithBranch } from '../../../types';
|
import { GitCommitWithBranch } from '../../../types';
|
||||||
@@ -11,10 +11,9 @@ interface ActivityCardProps {
|
|||||||
|
|
||||||
const ActivityCard = ({ activity }: ActivityCardProps) => {
|
const ActivityCard = ({ activity }: ActivityCardProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="group flex gap-2 hover:bg-gray-200 rounded mt-1">
|
<div className="group flex hover:bg-gray-200 rounded mt-1">
|
||||||
<div className="w-8">
|
<div className="w-4">^</div>
|
||||||
<Avatar src={activity.author?.avatar_url} variant="rounded" size="sm" />
|
|
||||||
</div>
|
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<Typography>{activity.commit.author?.name}</Typography>
|
<Typography>{activity.commit.author?.name}</Typography>
|
||||||
<Typography variant="small" color="gray">
|
<Typography variant="small" color="gray">
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Chip,
|
Chip,
|
||||||
ChipProps,
|
ChipProps,
|
||||||
Tooltip,
|
|
||||||
} from '@material-tailwind/react';
|
} from '@material-tailwind/react';
|
||||||
|
|
||||||
import { relativeTimeMs } from '../../../../utils/time';
|
import { relativeTimeMs } from '../../../../utils/time';
|
||||||
@@ -25,7 +24,6 @@ import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
|
|||||||
import AssignDomainDialog from './AssignDomainDialog';
|
import AssignDomainDialog from './AssignDomainDialog';
|
||||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||||
import { formatAddress } from '../../../../utils/format';
|
|
||||||
|
|
||||||
interface DeployDetailsCardProps {
|
interface DeployDetailsCardProps {
|
||||||
deployment: Deployment;
|
deployment: Deployment;
|
||||||
@@ -89,12 +87,16 @@ const DeploymentDetailsCard = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-8 gap-2 border-b border-gray-300 p-3 my-2">
|
<div className="grid grid-cols-4 gap-2 border-b border-gray-300 p-3 my-2">
|
||||||
<div className="col-span-3">
|
<div className="col-span-2">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{deployment.url && (
|
<Typography className=" basis-3/4">{deployment.url}</Typography>
|
||||||
<Typography className=" basis-3/4">{deployment.url}</Typography>
|
<Chip
|
||||||
)}
|
value={deployment.status}
|
||||||
|
color={STATUS_COLORS[deployment.status] ?? 'gray'}
|
||||||
|
variant="ghost"
|
||||||
|
icon={<i>^</i>}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Typography color="gray">
|
<Typography color="gray">
|
||||||
{deployment.environment === Environment.Production
|
{deployment.environment === Environment.Production
|
||||||
@@ -103,34 +105,27 @@ const DeploymentDetailsCard = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1">
|
<div className="col-span-1">
|
||||||
<Chip
|
|
||||||
value={deployment.status}
|
|
||||||
color={STATUS_COLORS[deployment.status] ?? 'gray'}
|
|
||||||
variant="ghost"
|
|
||||||
icon={<i>^</i>}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2">
|
|
||||||
<Typography color="gray">^ {deployment.branch}</Typography>
|
<Typography color="gray">^ {deployment.branch}</Typography>
|
||||||
<Typography color="gray">
|
<Typography color="gray">
|
||||||
^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
^ {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||||
{deployment.commitMessage}
|
{deployment.commitMessage}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 flex items-center">
|
<div className="col-span-1 flex items-center">
|
||||||
<Typography color="gray" className="grow">
|
<Typography color="gray" className="grow">
|
||||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
^ {relativeTimeMs(deployment.createdAt)} ^ {deployment.createdBy.name}
|
||||||
<Tooltip content={deployment.createdBy.name}>
|
|
||||||
{formatAddress(deployment.createdBy.name ?? '')}
|
|
||||||
</Tooltip>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Menu placement="bottom-start">
|
<Menu placement="bottom-start">
|
||||||
<MenuHandler>
|
<MenuHandler>
|
||||||
<button className="self-start">...</button>
|
<button className="self-start">...</button>
|
||||||
</MenuHandler>
|
</MenuHandler>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<a href={deployment.url} target="_blank" rel="noreferrer">
|
<a
|
||||||
<MenuItem disabled={!Boolean(deployment.url)}>^ Visit</MenuItem>
|
href={'https://' + deployment.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<MenuItem>^ Visit</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
|
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Typography, Chip, Card } from '@material-tailwind/react';
|
|||||||
import { color } from '@material-tailwind/react/types/components/chip';
|
import { color } from '@material-tailwind/react/types/components/chip';
|
||||||
import { relativeTimeMs } from '../../../../utils/time';
|
import { relativeTimeMs } from '../../../../utils/time';
|
||||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||||
import { formatAddress } from '../../../../utils/format';
|
|
||||||
|
|
||||||
interface DeploymentDialogBodyCardProps {
|
interface DeploymentDialogBodyCardProps {
|
||||||
deployment: Deployment;
|
deployment: Deployment;
|
||||||
@@ -29,19 +28,16 @@ const DeploymentDialogBodyCard = ({
|
|||||||
color={chip.color}
|
color={chip.color}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{deployment.url && (
|
<Typography variant="small" className="text-black">
|
||||||
<Typography variant="small" className="text-black">
|
{deployment.url}
|
||||||
{deployment.url}
|
</Typography>
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<Typography variant="small">
|
<Typography variant="small">
|
||||||
^ {deployment.branch} ^{' '}
|
^ {deployment.branch} ^{' '}
|
||||||
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
{deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '}
|
||||||
{deployment.commitMessage}
|
{deployment.commitMessage}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="small">
|
<Typography variant="small">
|
||||||
^ {relativeTimeMs(deployment.createdAt)} ^{' '}
|
^ {relativeTimeMs(deployment.createdAt)} ^ {deployment.createdBy.name}
|
||||||
{formatAddress(deployment.createdBy.name ?? '')}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,18 +44,18 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
|||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-8 gap-2 text-sm text-gray-600">
|
<div className="grid grid-cols-4 gap-2 text-sm text-gray-600">
|
||||||
<div className="col-span-4">
|
<div className="col-span-2">
|
||||||
<SearchBar
|
<SearchBar
|
||||||
placeholder="Search branches"
|
placeholder="Search branches"
|
||||||
value={searchedBranch}
|
value={searchedBranch}
|
||||||
onChange={(event) => setSearchedBranch(event.target.value)}
|
onChange={(event) => setSearchedBranch(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-1">
|
||||||
<DatePicker mode="range" selected={dateRange} onSelect={setDateRange} />
|
<DatePicker mode="range" selected={dateRange} onSelect={setDateRange} />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 relative">
|
<div className="col-span-1 relative">
|
||||||
<Select
|
<Select
|
||||||
value={selectedStatus}
|
value={selectedStatus}
|
||||||
onChange={(value) => setSelectedStatus(value as StatusOptions)}
|
onChange={(value) => setSelectedStatus(value as StatusOptions)}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Card, Collapse, Typography } from '@material-tailwind/react';
|
import { Card, Collapse, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
import { Environment, EnvironmentVariable } from 'gql-client/dist/src/types';
|
|
||||||
|
|
||||||
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
|
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
|
||||||
|
import { Environment, EnvironmentVariable } from 'gql-client';
|
||||||
|
|
||||||
interface DisplayEnvironmentVariablesProps {
|
interface DisplayEnvironmentVariablesProps {
|
||||||
environment: Environment;
|
environment: Environment;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
} from '@material-tailwind/react';
|
} from '@material-tailwind/react';
|
||||||
|
|
||||||
|
import { RepositoryDetails } from '../../../../types';
|
||||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||||
import EditDomainDialog from './EditDomainDialog';
|
import EditDomainDialog from './EditDomainDialog';
|
||||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||||
@@ -26,7 +27,7 @@ enum RefreshStatus {
|
|||||||
interface DomainCardProps {
|
interface DomainCardProps {
|
||||||
domains: Domain[];
|
domains: Domain[];
|
||||||
domain: Domain;
|
domain: Domain;
|
||||||
branches: string[];
|
repo: RepositoryDetails;
|
||||||
project: Project;
|
project: Project;
|
||||||
onUpdate: () => Promise<void>;
|
onUpdate: () => Promise<void>;
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,7 @@ const DOMAIN_RECORD = {
|
|||||||
const DomainCard = ({
|
const DomainCard = ({
|
||||||
domains,
|
domains,
|
||||||
domain,
|
domain,
|
||||||
branches,
|
repo,
|
||||||
project,
|
project,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
}: DomainCardProps) => {
|
}: DomainCardProps) => {
|
||||||
@@ -187,7 +188,7 @@ const DomainCard = ({
|
|||||||
domains={domains}
|
domains={domains}
|
||||||
open={editDialogOpen}
|
open={editDialogOpen}
|
||||||
domain={domain}
|
domain={domain}
|
||||||
branches={branches}
|
repo={repo}
|
||||||
onUpdate={onUpdate}
|
onUpdate={onUpdate}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Option,
|
Option,
|
||||||
} from '@material-tailwind/react';
|
} from '@material-tailwind/react';
|
||||||
|
|
||||||
|
import { RepositoryDetails } from '../../../../types';
|
||||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||||
|
|
||||||
const DEFAULT_REDIRECT_OPTIONS = ['none'];
|
const DEFAULT_REDIRECT_OPTIONS = ['none'];
|
||||||
@@ -24,7 +25,7 @@ interface EditDomainDialogProp {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
handleOpen: () => void;
|
handleOpen: () => void;
|
||||||
domain: Domain;
|
domain: Domain;
|
||||||
branches: string[];
|
repo: RepositoryDetails;
|
||||||
onUpdate: () => Promise<void>;
|
onUpdate: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ const EditDomainDialog = ({
|
|||||||
open,
|
open,
|
||||||
handleOpen,
|
handleOpen,
|
||||||
domain,
|
domain,
|
||||||
branches,
|
repo,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
}: EditDomainDialogProp) => {
|
}: EditDomainDialogProp) => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
@@ -119,7 +120,7 @@ const EditDomainDialog = ({
|
|||||||
branch: domain.branch,
|
branch: domain.branch,
|
||||||
redirectedTo: getRedirectUrl(domain),
|
redirectedTo: getRedirectUrl(domain),
|
||||||
});
|
});
|
||||||
}, [domain]);
|
}, [domain, repo]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} handler={handleOpen}>
|
<Dialog open={open} handler={handleOpen}>
|
||||||
@@ -165,13 +166,9 @@ const EditDomainDialog = ({
|
|||||||
<Input
|
<Input
|
||||||
crossOrigin={undefined}
|
crossOrigin={undefined}
|
||||||
{...register('branch', {
|
{...register('branch', {
|
||||||
validate: (value) =>
|
validate: (value) => repo.branch.includes(value),
|
||||||
Boolean(branches.length) ? branches.includes(value) : true,
|
|
||||||
})}
|
})}
|
||||||
disabled={
|
disabled={watch('redirectedTo') !== DEFAULT_REDIRECT_OPTIONS[0]}
|
||||||
!Boolean(branches.length) ||
|
|
||||||
watch('redirectedTo') !== DEFAULT_REDIRECT_OPTIONS[0]
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
{!isValid && (
|
{!isValid && (
|
||||||
<Typography variant="small" className="text-red-500">
|
<Typography variant="small" className="text-red-500">
|
||||||
|
|||||||
@@ -7,11 +7,9 @@ import {
|
|||||||
Option,
|
Option,
|
||||||
Chip,
|
Chip,
|
||||||
IconButton,
|
IconButton,
|
||||||
Tooltip,
|
|
||||||
} from '@material-tailwind/react';
|
} from '@material-tailwind/react';
|
||||||
|
|
||||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||||
import { formatAddress } from '../../../../utils/format';
|
|
||||||
|
|
||||||
const PERMISSION_OPTIONS = [
|
const PERMISSION_OPTIONS = [
|
||||||
{
|
{
|
||||||
@@ -50,7 +48,6 @@ const MemberCard = ({
|
|||||||
onRemoveProjectMember,
|
onRemoveProjectMember,
|
||||||
onUpdateProjectMember,
|
onUpdateProjectMember,
|
||||||
}: MemberCardProps) => {
|
}: MemberCardProps) => {
|
||||||
const [ethAddress, emailDomain] = member.email.split('@');
|
|
||||||
const [selectedPermission, setSelectedPermission] = useState(
|
const [selectedPermission, setSelectedPermission] = useState(
|
||||||
permissions.join('+'),
|
permissions.join('+'),
|
||||||
);
|
);
|
||||||
@@ -82,16 +79,8 @@ const MemberCard = ({
|
|||||||
>
|
>
|
||||||
<div>^</div>
|
<div>^</div>
|
||||||
<div className="basis-1/2">
|
<div className="basis-1/2">
|
||||||
{member.name && (
|
{member.name && <Typography variant="small">{member.name}</Typography>}
|
||||||
<Tooltip content={member.name}>
|
<Typography variant="small">{member.email}</Typography>
|
||||||
{formatAddress(member.name ?? '')}
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<Tooltip content={member.email}>
|
|
||||||
<p>
|
|
||||||
{formatAddress(ethAddress)}@{emailDomain}
|
|
||||||
</p>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="basis-1/2">
|
<div className="basis-1/2">
|
||||||
{!isPending ? (
|
{!isPending ? (
|
||||||
@@ -153,9 +142,8 @@ const MemberCard = ({
|
|||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
<Typography variant="small">
|
<Typography variant="small">
|
||||||
Once removed, {formatAddress(member.name ?? '')} (
|
Once removed, {member.name} ({member.email}) will not be able to
|
||||||
{formatAddress(ethAddress)}@{emailDomain}) will not be able to access
|
access this project.
|
||||||
this project.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { tv } from 'tailwind-variants';
|
||||||
|
import type { VariantProps } from 'tailwind-variants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the theme for a button component.
|
||||||
|
*/
|
||||||
|
export const buttonTheme = tv(
|
||||||
|
{
|
||||||
|
base: [
|
||||||
|
'h-fit',
|
||||||
|
'inline-flex',
|
||||||
|
'items-center',
|
||||||
|
'justify-center',
|
||||||
|
'whitespace-nowrap',
|
||||||
|
'focus-ring',
|
||||||
|
'disabled:cursor-not-allowed',
|
||||||
|
],
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
lg: ['gap-2', 'py-3.5', 'px-5', 'text-base', 'tracking-[-0.011em]'],
|
||||||
|
md: ['gap-2', 'py-3.25', 'px-5', 'text-sm', 'tracking-[-0.006em]'],
|
||||||
|
sm: ['gap-1', 'py-2', 'px-3', 'text-xs'],
|
||||||
|
xs: ['gap-1', 'py-1', 'px-2', 'text-xs'],
|
||||||
|
},
|
||||||
|
fullWidth: {
|
||||||
|
true: 'w-full',
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
default: '',
|
||||||
|
rounded: 'rounded-full',
|
||||||
|
},
|
||||||
|
iconOnly: {
|
||||||
|
true: '',
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
primary: [
|
||||||
|
'text-elements-on-primary',
|
||||||
|
'border',
|
||||||
|
'border-transparent',
|
||||||
|
'bg-controls-primary',
|
||||||
|
'shadow-button',
|
||||||
|
'hover:bg-controls-primary-hovered',
|
||||||
|
'focus-visible:bg-controls-primary-hovered',
|
||||||
|
'disabled:text-elements-on-disabled',
|
||||||
|
'disabled:bg-controls-disabled',
|
||||||
|
'disabled:border-transparent',
|
||||||
|
'disabled:shadow-none',
|
||||||
|
],
|
||||||
|
secondary: [
|
||||||
|
'text-elements-on-secondary',
|
||||||
|
'border',
|
||||||
|
'border-transparent',
|
||||||
|
'bg-controls-secondary',
|
||||||
|
'hover:bg-controls-secondary-hovered',
|
||||||
|
'focus-visible:bg-controls-secondary-hovered',
|
||||||
|
'disabled:text-elements-on-disabled',
|
||||||
|
'disabled:bg-controls-disabled',
|
||||||
|
'disabled:border-transparent',
|
||||||
|
'disabled:shadow-none',
|
||||||
|
],
|
||||||
|
tertiary: [
|
||||||
|
'text-elements-on-tertiary',
|
||||||
|
'border',
|
||||||
|
'border-border-interactive/10',
|
||||||
|
'bg-transparent',
|
||||||
|
'hover:bg-controls-tertiary-hovered',
|
||||||
|
'hover:border-border-interactive-hovered',
|
||||||
|
'hover:border-border-interactive-hovered/[0.14]',
|
||||||
|
'focus-visible:bg-controls-tertiary-hovered',
|
||||||
|
'focus-visible:border-border-interactive-hovered',
|
||||||
|
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||||
|
'disabled:text-elements-on-disabled',
|
||||||
|
'disabled:bg-controls-disabled',
|
||||||
|
'disabled:border-transparent',
|
||||||
|
'disabled:shadow-none',
|
||||||
|
],
|
||||||
|
ghost: [
|
||||||
|
'text-elements-on-tertiary',
|
||||||
|
'border',
|
||||||
|
'border-transparent',
|
||||||
|
'bg-transparent',
|
||||||
|
'hover:bg-controls-tertiary-hovered',
|
||||||
|
'hover:border-border-interactive-hovered',
|
||||||
|
'hover:border-border-interactive-hovered/[0.14]',
|
||||||
|
'focus-visible:bg-controls-tertiary-hovered',
|
||||||
|
'focus-visible:border-border-interactive-hovered',
|
||||||
|
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||||
|
'disabled:text-elements-on-disabled',
|
||||||
|
'disabled:bg-controls-disabled',
|
||||||
|
'disabled:border-transparent',
|
||||||
|
'disabled:shadow-none',
|
||||||
|
],
|
||||||
|
danger: [
|
||||||
|
'text-elements-on-danger',
|
||||||
|
'border',
|
||||||
|
'border-transparent',
|
||||||
|
'bg-border-danger',
|
||||||
|
'hover:bg-controls-danger-hovered',
|
||||||
|
'focus-visible:bg-controls-danger-hovered',
|
||||||
|
'disabled:text-elements-on-disabled',
|
||||||
|
'disabled:bg-controls-disabled',
|
||||||
|
'disabled:border-transparent',
|
||||||
|
'disabled:shadow-none',
|
||||||
|
],
|
||||||
|
'danger-ghost': [
|
||||||
|
'text-elements-danger',
|
||||||
|
'border',
|
||||||
|
'border-transparent',
|
||||||
|
'bg-transparent',
|
||||||
|
'hover:bg-controls-tertiary-hovered',
|
||||||
|
'hover:border-border-interactive-hovered',
|
||||||
|
'hover:border-border-interactive-hovered/[0.14]',
|
||||||
|
'focus-visible:bg-controls-tertiary-hovered',
|
||||||
|
'focus-visible:border-border-interactive-hovered',
|
||||||
|
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||||
|
'disabled:text-elements-on-disabled',
|
||||||
|
'disabled:bg-controls-disabled',
|
||||||
|
'disabled:border-transparent',
|
||||||
|
'disabled:shadow-none',
|
||||||
|
],
|
||||||
|
unstyled: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
size: 'lg',
|
||||||
|
iconOnly: true,
|
||||||
|
class: ['py-3.5', 'px-3.5'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
size: 'md',
|
||||||
|
iconOnly: true,
|
||||||
|
class: ['py-3.25', 'px-3.25'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
size: 'sm',
|
||||||
|
iconOnly: true,
|
||||||
|
class: ['py-2', 'px-2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
size: 'xs',
|
||||||
|
iconOnly: true,
|
||||||
|
class: ['py-1', 'px-1'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultVariants: {
|
||||||
|
size: 'md',
|
||||||
|
variant: 'primary',
|
||||||
|
fullWidth: false,
|
||||||
|
iconOnly: false,
|
||||||
|
shape: 'rounded',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
responsiveVariants: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the type of a button theme, which is derived from the `buttonTheme` variant props.
|
||||||
|
*/
|
||||||
|
export type ButtonTheme = VariantProps<typeof buttonTheme>;
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import React, { forwardRef, useCallback } from 'react';
|
||||||
|
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { buttonTheme } from './Button.theme';
|
||||||
|
import type { ButtonTheme } from './Button.theme';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { cloneIcon } from 'utils/cloneIcon';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the properties of a base button component.
|
||||||
|
*/
|
||||||
|
interface ButtonBaseProps {
|
||||||
|
/**
|
||||||
|
* The optional left icon element for a component.
|
||||||
|
* @type {ReactNode}
|
||||||
|
*/
|
||||||
|
leftIcon?: ReactNode;
|
||||||
|
/**
|
||||||
|
* The optional right icon element to display.
|
||||||
|
* @type {ReactNode}
|
||||||
|
*/
|
||||||
|
rightIcon?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for the props of a button link component.
|
||||||
|
*/
|
||||||
|
export interface ButtonLinkProps
|
||||||
|
extends Omit<ComponentPropsWithoutRef<'a'>, 'color'> {
|
||||||
|
/**
|
||||||
|
* Specifies the optional property `as` with a value of `'a'`.
|
||||||
|
* @type {'a'}
|
||||||
|
*/
|
||||||
|
as?: 'a';
|
||||||
|
/**
|
||||||
|
* Indicates whether the item is external or not.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
external?: boolean;
|
||||||
|
/**
|
||||||
|
* The URL of a web page or resource.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends Omit<ComponentPropsWithoutRef<'button'>, 'color'> {
|
||||||
|
/**
|
||||||
|
* Specifies the optional property `as` with a value of `'button'`.
|
||||||
|
* @type {'button'}
|
||||||
|
*/
|
||||||
|
as?: 'button';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface representing the props for a button component.
|
||||||
|
* Extends the ComponentPropsWithoutRef<'button'> and ButtonTheme interfaces.
|
||||||
|
*/
|
||||||
|
export type ButtonOrLinkProps = (ButtonLinkProps | ButtonProps) &
|
||||||
|
ButtonBaseProps &
|
||||||
|
ButtonTheme;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom button component that can be used in React applications.
|
||||||
|
*/
|
||||||
|
const Button = forwardRef<
|
||||||
|
HTMLButtonElement | HTMLAnchorElement,
|
||||||
|
ButtonOrLinkProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
leftIcon,
|
||||||
|
rightIcon,
|
||||||
|
fullWidth,
|
||||||
|
iconOnly,
|
||||||
|
shape,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
// Conditionally render between <NextLink>, <a> or <button> depending on props
|
||||||
|
// useCallback to prevent unnecessary re-rendering
|
||||||
|
const Component = useCallback(
|
||||||
|
({ children: _children, ..._props }: ButtonOrLinkProps) => {
|
||||||
|
if (_props.as === 'a') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { external, href, as, ...baseLinkProps } = _props;
|
||||||
|
|
||||||
|
// External link
|
||||||
|
if (external) {
|
||||||
|
const externalLinkProps = {
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener',
|
||||||
|
href,
|
||||||
|
...baseLinkProps,
|
||||||
|
};
|
||||||
|
return <a {...externalLinkProps}>{_children}</a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal link
|
||||||
|
return (
|
||||||
|
<Link {...baseLinkProps} to={href}>
|
||||||
|
{_children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const { ...buttonProps } = _props;
|
||||||
|
// @ts-expect-error - as prop is not a valid prop for button elements
|
||||||
|
return <button {...buttonProps}>{_children}</button>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts specific style properties from the given props object and returns them as a new object.
|
||||||
|
*/
|
||||||
|
const styleProps = (({
|
||||||
|
variant = 'primary',
|
||||||
|
size = 'md',
|
||||||
|
fullWidth = false,
|
||||||
|
iconOnly = false,
|
||||||
|
shape = 'rounded',
|
||||||
|
as,
|
||||||
|
}) => ({
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
fullWidth,
|
||||||
|
iconOnly,
|
||||||
|
shape,
|
||||||
|
as,
|
||||||
|
}))({ ...props, fullWidth, iconOnly, shape, variant });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a button component has either children or an aria-label prop.
|
||||||
|
*/
|
||||||
|
if (typeof children === 'undefined' && !props['aria-label']) {
|
||||||
|
throw new Error(
|
||||||
|
'Button components must have either children or an aria-label prop',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconSize = useCallback(() => {
|
||||||
|
switch (styleProps.size) {
|
||||||
|
case 'lg':
|
||||||
|
return { width: 20, height: 20 };
|
||||||
|
case 'sm':
|
||||||
|
case 'xs':
|
||||||
|
return { width: 16, height: 16 };
|
||||||
|
case 'md':
|
||||||
|
default: {
|
||||||
|
return { width: 18, height: 18 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [styleProps.size])();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
{...props}
|
||||||
|
// @ts-expect-error - ref is not a valid prop for button elements
|
||||||
|
ref={ref}
|
||||||
|
className={buttonTheme({ ...styleProps, class: className })}
|
||||||
|
>
|
||||||
|
{cloneIcon(leftIcon, { ...iconSize })}
|
||||||
|
{children}
|
||||||
|
{cloneIcon(rightIcon, { ...iconSize })}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
|
export { Button };
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './Button';
|
||||||
|
export * from './Button.theme';
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import React, { ComponentPropsWithoutRef } from 'react';
|
||||||
|
|
||||||
|
export interface CustomIconProps extends ComponentPropsWithoutRef<'svg'> {
|
||||||
|
size?: number | string; // width and height will both be set as the same value
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomIcon = ({
|
||||||
|
children,
|
||||||
|
width = 24,
|
||||||
|
height = 24,
|
||||||
|
size,
|
||||||
|
viewBox = '0 0 24 24',
|
||||||
|
name,
|
||||||
|
...rest
|
||||||
|
}: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-labelledby={name}
|
||||||
|
height={size || height}
|
||||||
|
role="presentation"
|
||||||
|
viewBox={viewBox}
|
||||||
|
width={size || width}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||||
|
|
||||||
|
export const PlusIcon = (props: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<CustomIcon
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M1.66666 9.99999C1.66666 5.39762 5.39762 1.66666 9.99999 1.66666C14.6024 1.66666 18.3333 5.39762 18.3333 9.99999C18.3333 14.6024 14.6024 18.3333 9.99999 18.3333C5.39762 18.3333 1.66666 14.6024 1.66666 9.99999ZM10.625 6.46483C10.625 6.11966 10.3452 5.83983 9.99999 5.83983C9.65481 5.83983 9.37499 6.11966 9.37499 6.46483V9.37537H6.46446C6.11928 9.37537 5.83946 9.65519 5.83946 10.0004C5.83946 10.3455 6.11928 10.6254 6.46446 10.6254H9.37499V13.5359C9.37499 13.8811 9.65481 14.1609 9.99999 14.1609C10.3452 14.1609 10.625 13.8811 10.625 13.5359V10.6254H13.5355C13.8807 10.6254 14.1605 10.3455 14.1605 10.0004C14.1605 9.65519 13.8807 9.37537 13.5355 9.37537H10.625V6.46483Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</CustomIcon>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# 1. What icons are compatible with this component?
|
||||||
|
|
||||||
|
- Viewbox "0 0 24 24": From where you're exporting from, please make sure the icon is using viewBox="0 0 24 24" before downloading/exporting. Not doing so will result in incorrect icon scaling
|
||||||
|
|
||||||
|
# 2. How to add a new icon?
|
||||||
|
|
||||||
|
**2.1 Sanitising the icon**
|
||||||
|
|
||||||
|
1. Duplicate a current icon e.g. CrossIcon and rename it accordingly.
|
||||||
|
2. Rename the function inside the new file you duplicated too
|
||||||
|
3. Replace the markup with your SVG markup (make sure it complies with the above section's rule)
|
||||||
|
4. Depending on the svg you pasted...
|
||||||
|
A. If the `<svg>` has only 1 child, remove the `<svg>` parent entirely so you only have the path left
|
||||||
|
B. If your component has more than 1 paths, rename `<svg>` tag with the `<g>` tag. Then, remove all attributes of this `<g>` tag so that it's just `<g>`
|
||||||
|
5. Usually, icons are single colored. If that's the case, replace all fill/stroke color with `currentColor`. E.g. <path d="..." fill="currentColor">. Leave the other attributes without removing them.
|
||||||
|
6. If your icon has more than one colour, then it's up to you to decide whether we want to use tailwind to help set the fill and stroke colors
|
||||||
|
7. Lastly, export your icon in `index.ts` by following what was done for CrossIcon
|
||||||
|
8. Make sure to provide a name to the `<CustomIcon>` component for accessibility sake
|
||||||
|
9. Done!
|
||||||
|
|
||||||
|
**2.3 Use your newly imported icon**
|
||||||
|
|
||||||
|
1. You can change simply use `<BellIcon size="32" />` to quickly change both width and height with the same value (square). For custom viewBox, width and height, simply provide all three props.
|
||||||
|
2. Coloring the icon: Simply add a className with text color. E.g. `<BellIcon className="text-gray-500" />`
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './PlusIcon';
|
||||||
|
export * from './CustomIcon';
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
export const GIT_TEMPLATE_LINK = `https://github.com/${process.env.REACT_APP_GITHUB_TEMPLATE_REPO}`;
|
export const GIT_TEMPLATE_LINK =
|
||||||
|
'https://git.vdb.to/cerc-io/test-progressive-web-app';
|
||||||
|
|
||||||
export const SHORT_COMMIT_HASH_LENGTH = 8;
|
export const SHORT_COMMIT_HASH_LENGTH = 8;
|
||||||
|
|
||||||
export const SERVER_GQL_PATH = 'graphql';
|
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import { useGQLClient } from './GQLClientContext';
|
|||||||
const UNAUTHORIZED_ERROR_CODE = 401;
|
const UNAUTHORIZED_ERROR_CODE = 401;
|
||||||
|
|
||||||
interface ContextValue {
|
interface ContextValue {
|
||||||
octokit: Octokit;
|
octokit: Octokit | null;
|
||||||
isAuth: boolean;
|
isAuth: boolean;
|
||||||
updateAuth: () => void;
|
updateAuth: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OctokitContext = createContext<ContextValue>({
|
const OctokitContext = createContext<ContextValue>({
|
||||||
octokit: new Octokit(),
|
octokit: null,
|
||||||
isAuth: false,
|
isAuth: false,
|
||||||
updateAuth: () => {},
|
updateAuth: () => {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
import React, { ReactNode } from 'react';
|
|
||||||
import { SiweMessage } from 'siwe';
|
|
||||||
import { WagmiProvider } from 'wagmi';
|
|
||||||
import { arbitrum, mainnet } from 'wagmi/chains';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
import { createWeb3Modal } from '@web3modal/wagmi/react';
|
|
||||||
import { defaultWagmiConfig } from '@web3modal/wagmi/react/config';
|
|
||||||
import { createSIWEConfig } from '@web3modal/siwe';
|
|
||||||
import type {
|
|
||||||
SIWECreateMessageArgs,
|
|
||||||
SIWEVerifyMessageArgs,
|
|
||||||
} from '@web3modal/core';
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
const axiosInstance = axios.create({
|
|
||||||
baseURL: process.env.REACT_APP_SERVER_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
},
|
|
||||||
withCredentials: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const metadata = {
|
|
||||||
name: 'Web3Modal',
|
|
||||||
description: 'Snowball Web3Modal',
|
|
||||||
url: window.location.origin,
|
|
||||||
icons: ['https://avatars.githubusercontent.com/u/37784886'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const chains = [mainnet, arbitrum] as const;
|
|
||||||
const config = defaultWagmiConfig({
|
|
||||||
chains,
|
|
||||||
projectId: process.env.REACT_APP_WALLET_CONNECT_ID,
|
|
||||||
metadata,
|
|
||||||
});
|
|
||||||
|
|
||||||
const siweConfig = createSIWEConfig({
|
|
||||||
createMessage: ({ nonce, address, chainId }: SIWECreateMessageArgs) =>
|
|
||||||
new SiweMessage({
|
|
||||||
version: '1',
|
|
||||||
domain: window.location.host,
|
|
||||||
uri: window.location.origin,
|
|
||||||
address,
|
|
||||||
chainId,
|
|
||||||
nonce,
|
|
||||||
// Human-readable ASCII assertion that the user will sign, and it must not contain `\n`.
|
|
||||||
statement: 'Sign in With Ethereum.',
|
|
||||||
}).prepareMessage(),
|
|
||||||
getNonce: async () => {
|
|
||||||
const nonce = (await axiosInstance.get('/auth/nonce')).data;
|
|
||||||
if (!nonce) {
|
|
||||||
throw new Error('Failed to get nonce!');
|
|
||||||
}
|
|
||||||
|
|
||||||
return nonce;
|
|
||||||
},
|
|
||||||
getSession: async () => {
|
|
||||||
try {
|
|
||||||
const session = (await axiosInstance.get('/auth/session')).data;
|
|
||||||
const { address, chainId } = session;
|
|
||||||
|
|
||||||
return { address, chainId };
|
|
||||||
} catch (err) {
|
|
||||||
if (window.location.pathname !== '/login') {
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Failed to get session!');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
verifyMessage: async ({ message, signature }: SIWEVerifyMessageArgs) => {
|
|
||||||
try {
|
|
||||||
const { success } = (
|
|
||||||
await axiosInstance.post('/auth/validate', {
|
|
||||||
message,
|
|
||||||
signature,
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
|
|
||||||
return success;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
signOut: async () => {
|
|
||||||
try {
|
|
||||||
const { success } = (await axiosInstance.post('/auth/logout')).data;
|
|
||||||
return success;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSignOut: () => {
|
|
||||||
window.location.href = '/login';
|
|
||||||
},
|
|
||||||
onSignIn: () => {
|
|
||||||
window.location.href = '/';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!process.env.REACT_APP_WALLET_CONNECT_ID) {
|
|
||||||
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
|
|
||||||
}
|
|
||||||
|
|
||||||
createWeb3Modal({
|
|
||||||
siweConfig,
|
|
||||||
wagmiConfig: config,
|
|
||||||
projectId: process.env.REACT_APP_WALLET_CONNECT_ID,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function Web3ModalProvider({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<WagmiProvider config={config}>
|
|
||||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
||||||
</WagmiProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.focus-ring {
|
||||||
|
@apply focus-visible:ring-[3px] focus-visible:ring-snowball-200 focus-visible:ring-offset-1 focus-visible:ring-offset-snowball-500 focus-visible:outline-none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,33 +7,27 @@ import { GQLClient } from 'gql-client';
|
|||||||
import { ThemeProvider } from '@material-tailwind/react';
|
import { ThemeProvider } from '@material-tailwind/react';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
import '@fontsource/inter';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
import { GQLClientProvider } from './context/GQLClientContext';
|
import { GQLClientProvider } from './context/GQLClientContext';
|
||||||
import Web3ModalProvider from './context/Web3ModalProvider';
|
|
||||||
import { SERVER_GQL_PATH } from './constants';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement,
|
document.getElementById('root') as HTMLElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(
|
const gqlEndpoint = process.env.REACT_APP_GQL_SERVER_URL;
|
||||||
process.env.REACT_APP_SERVER_URL,
|
assert(gqlEndpoint, 'GQL server URL not provided');
|
||||||
'REACT_APP_SERVER_URL is not set in env',
|
|
||||||
);
|
|
||||||
const gqlEndpoint = `${process.env.REACT_APP_SERVER_URL}/${SERVER_GQL_PATH}`;
|
|
||||||
|
|
||||||
const gqlClient = new GQLClient({ gqlEndpoint });
|
const gqlClient = new GQLClient({ gqlEndpoint });
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Web3ModalProvider>
|
<GQLClientProvider client={gqlClient}>
|
||||||
<GQLClientProvider client={gqlClient}>
|
<App />
|
||||||
<App />
|
<Toaster position="bottom-center" />
|
||||||
<Toaster position="bottom-center" />
|
</GQLClientProvider>
|
||||||
</GQLClientProvider>
|
|
||||||
</Web3ModalProvider>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,27 +1,13 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { Outlet, useNavigate } from 'react-router-dom';
|
import { Outlet, useNavigate } from 'react-router-dom';
|
||||||
import { User } from 'gql-client';
|
|
||||||
|
|
||||||
import { IconButton, Tooltip, Typography } from '@material-tailwind/react';
|
import { IconButton, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
import HorizontalLine from '../components/HorizontalLine';
|
import HorizontalLine from '../components/HorizontalLine';
|
||||||
import ProjectSearchBar from '../components/projects/ProjectSearchBar';
|
import ProjectSearchBar from '../components/projects/ProjectSearchBar';
|
||||||
import { useGQLClient } from '../context/GQLClientContext';
|
|
||||||
import { formatAddress } from '../utils/format';
|
|
||||||
|
|
||||||
const ProjectSearch = () => {
|
const ProjectSearch = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const client = useGQLClient();
|
|
||||||
const [user, setUser] = useState<User>();
|
|
||||||
|
|
||||||
const fetchUser = useCallback(async () => {
|
|
||||||
const { user } = await client.getUser();
|
|
||||||
setUser(user);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchUser();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -42,10 +28,8 @@ const ProjectSearch = () => {
|
|||||||
<div className="mr-2 flex items-center">
|
<div className="mr-2 flex items-center">
|
||||||
<Typography>^</Typography>
|
<Typography>^</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-1 bg-blue-gray-50 rounded-lg flex items-center">
|
<div className="px-2 py-1 bg-blue-gray-50 rounded-lg">
|
||||||
{user?.name && (
|
<Typography variant="lead">SY</Typography>
|
||||||
<Tooltip content={user.name}>{formatAddress(user.name)}</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HorizontalLine />
|
<HorizontalLine />
|
||||||
|
|||||||
@@ -1 +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>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -1,14 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const Login = () => {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-5 h-screen bg-light-blue-50 py-10">
|
|
||||||
<div className="col-span-2"></div>
|
|
||||||
<div className="col-span-1">
|
|
||||||
<w3m-button />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Login;
|
|
||||||
@@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
import Sidebar from '../components/Sidebar';
|
import Sidebar from '../components/Sidebar';
|
||||||
import { OctokitProvider } from '../context/OctokitContext';
|
|
||||||
|
|
||||||
const OrgSlug = () => {
|
const OrgSlug = () => {
|
||||||
return (
|
return (
|
||||||
@@ -13,9 +12,7 @@ const OrgSlug = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-span-4 h-full p-3 overflow-y-hidden">
|
<div className="col-span-4 h-full p-3 overflow-y-hidden">
|
||||||
<div className="bg-white rounded-3xl h-full overflow-y-auto">
|
<div className="bg-white rounded-3xl h-full overflow-y-auto">
|
||||||
<OctokitProvider>
|
<Outlet />
|
||||||
<Outlet />
|
|
||||||
</OctokitProvider>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { Button, ButtonOrLinkProps } from 'components/shared/Button';
|
||||||
|
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return (
|
||||||
|
<div className="relative h-full min-h-full">
|
||||||
|
<div className="flex flex-col items-center justify-center max-w-7xl mx-auto px-20 py-20">
|
||||||
|
<h1 className="text-4xl font-bold">Manual Storybook</h1>
|
||||||
|
<p className="mt-4 text-lg text-center text-gray-500">
|
||||||
|
Get started by editing{' '}
|
||||||
|
<code className="p-2 font-mono text-sm bg-gray-100 rounded-md">
|
||||||
|
packages/frontend/src/pages/components/index.tsx
|
||||||
|
</code>
|
||||||
|
</p>
|
||||||
|
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||||
|
|
||||||
|
{/* Insert Components here */}
|
||||||
|
<div className="flex flex-col gap-10 items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-bold">Button</h1>
|
||||||
|
<div className="flex flex-col gap-10">
|
||||||
|
{['primary', 'secondary', 'tertiary', 'danger'].map(
|
||||||
|
(variant, index) => (
|
||||||
|
<div className="flex gap-5 flex-wrap" key={index}>
|
||||||
|
{['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => (
|
||||||
|
<Button
|
||||||
|
leftIcon={<PlusIcon />}
|
||||||
|
rightIcon={<PlusIcon />}
|
||||||
|
variant={variant as ButtonOrLinkProps['variant']}
|
||||||
|
size={
|
||||||
|
size !== 'disabled'
|
||||||
|
? (size as ButtonOrLinkProps['size'])
|
||||||
|
: 'md'
|
||||||
|
}
|
||||||
|
key={`${variant}-${size}`}
|
||||||
|
disabled={size === 'disabled'}
|
||||||
|
>
|
||||||
|
Button
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
{[
|
||||||
|
'primary',
|
||||||
|
'secondary',
|
||||||
|
'tertiary',
|
||||||
|
'ghost',
|
||||||
|
'danger',
|
||||||
|
'danger-ghost',
|
||||||
|
].map((variant, index) => (
|
||||||
|
<div className="flex gap-5 flex-wrap" key={index}>
|
||||||
|
{['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => (
|
||||||
|
<Button
|
||||||
|
iconOnly
|
||||||
|
variant={variant as ButtonOrLinkProps['variant']}
|
||||||
|
size={
|
||||||
|
size !== 'disabled'
|
||||||
|
? (size as ButtonOrLinkProps['size'])
|
||||||
|
: 'md'
|
||||||
|
}
|
||||||
|
key={`${variant}-${size}`}
|
||||||
|
disabled={size === 'disabled'}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -19,31 +19,19 @@ import {
|
|||||||
|
|
||||||
import HorizontalLine from '../../../components/HorizontalLine';
|
import HorizontalLine from '../../../components/HorizontalLine';
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
import { useOctokit } from '../../../context/OctokitContext';
|
|
||||||
|
|
||||||
const Id = () => {
|
const Id = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { octokit } = useOctokit();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const [project, setProject] = useState<ProjectType | null>(null);
|
const [project, setProject] = useState<ProjectType | null>(null);
|
||||||
const [repoUrl, setRepoUrl] = useState('');
|
|
||||||
|
|
||||||
const fetchProject = useCallback(async (id: string | undefined) => {
|
const fetchProject = useCallback(async (id: string | undefined) => {
|
||||||
if (id) {
|
if (id) {
|
||||||
const { project } = await client.getProject(id);
|
const { project } = await client.getProject(id);
|
||||||
setProject(project);
|
setProject(project);
|
||||||
|
|
||||||
if (project) {
|
|
||||||
const [owner, repo] = project.repository.split('/');
|
|
||||||
const { data: repoDetails } = await octokit.rest.repos.get({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
});
|
|
||||||
setRepoUrl(repoDetails.html_url);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -80,11 +68,9 @@ const Id = () => {
|
|||||||
<Typography variant="h3" className="grow">
|
<Typography variant="h3" className="grow">
|
||||||
{project?.name}
|
{project?.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Link to={repoUrl} target="_blank">
|
<Button className="rounded-full" variant="outlined">
|
||||||
<Button className="rounded-full" variant="outlined">
|
Open Repo
|
||||||
Open Repo
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Button className="rounded-full" color="blue">
|
<Button className="rounded-full" color="blue">
|
||||||
Go to app
|
Go to app
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const CreateWithTemplate = () => {
|
|||||||
<div className="grow px-2">{template?.name}</div>
|
<div className="grow px-2">{template?.name}</div>
|
||||||
<div>
|
<div>
|
||||||
<a href={GIT_TEMPLATE_LINK} target="_blank" rel="noreferrer">
|
<a href={GIT_TEMPLATE_LINK} target="_blank" rel="noreferrer">
|
||||||
^ {process.env.REACT_APP_GITHUB_TEMPLATE_REPO}
|
^ cerc-io/test-progressive-web-app
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const NewProject = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<h5 className="mt-4 ml-4">Import a repository</h5>
|
<h5 className="mt-4 ml-4">Import a repository</h5>
|
||||||
<RepositoryList octokit={octokit} />
|
<RepositoryList octokit={octokit!} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<ConnectAccount onAuth={updateAuth} />
|
<ConnectAccount onAuth={updateAuth} />
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
|
||||||
import { Button, Option, Typography } from '@material-tailwind/react';
|
import { Option, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
import { useOctokit } from '../../../../../context/OctokitContext';
|
import { useOctokit } from '../../../../../context/OctokitContext';
|
||||||
import { useGQLClient } from '../../../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../../../context/GQLClientContext';
|
||||||
@@ -27,12 +27,10 @@ const CreateRepo = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [gitAccounts, setGitAccounts] = useState<string[]>([]);
|
const [gitAccounts, setGitAccounts] = useState<string[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const submitRepoHandler: SubmitHandler<SubmitRepoValues> = useCallback(
|
const submitRepoHandler: SubmitHandler<SubmitRepoValues> = useCallback(
|
||||||
async (data) => {
|
async (data) => {
|
||||||
assert(data.account);
|
assert(data.account);
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
assert(
|
assert(
|
||||||
@@ -64,17 +62,11 @@ const CreateRepo = () => {
|
|||||||
template: 'webapp',
|
template: 'webapp',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Boolean(addProject)) {
|
navigate(
|
||||||
setIsLoading(true);
|
`/${orgSlug}/projects/create/template/deploy?projectId=${addProject.id}`,
|
||||||
navigate(
|
);
|
||||||
`/${orgSlug}/projects/create/template/deploy?projectId=${addProject.id}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setIsLoading(false);
|
|
||||||
toast.error('Error deploying project');
|
toast.error('Error deploying project');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -182,14 +174,9 @@ const CreateRepo = () => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<Button
|
<button className="bg-blue-500 rounded-xl p-2" type="submit">
|
||||||
className="bg-blue-500 rounded-xl p-2"
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
Deploy ^
|
Deploy ^
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const DEFAULT_FILTER_VALUE: FilterValue = {
|
|||||||
searchedBranch: '',
|
searchedBranch: '',
|
||||||
status: StatusOptions.ALL_STATUS,
|
status: StatusOptions.ALL_STATUS,
|
||||||
};
|
};
|
||||||
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
|
|
||||||
|
|
||||||
const DeploymentsTabPanel = () => {
|
const DeploymentsTabPanel = () => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
@@ -27,30 +26,22 @@ const DeploymentsTabPanel = () => {
|
|||||||
const [deployments, setDeployments] = useState<Deployment[]>([]);
|
const [deployments, setDeployments] = useState<Deployment[]>([]);
|
||||||
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
|
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
|
||||||
|
|
||||||
const fetchDeployments = useCallback(async () => {
|
const fetchDeployments = async () => {
|
||||||
const { deployments } = await client.getDeployments(project.id);
|
const { deployments } = await client.getDeployments(project.id);
|
||||||
setDeployments(deployments);
|
setDeployments(deployments);
|
||||||
}, [client, project]);
|
};
|
||||||
|
|
||||||
const fetchProductionBranchDomains = useCallback(async () => {
|
const fetchProductionBranchDomains = useCallback(async () => {
|
||||||
const { domains } = await client.getDomains(project.id, {
|
const { domains } = await client.getDomains(project.id, {
|
||||||
branch: project.prodBranch,
|
branch: project.prodBranch,
|
||||||
});
|
});
|
||||||
setProdBranchDomains(domains);
|
setProdBranchDomains(domains);
|
||||||
}, [client, project]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProductionBranchDomains();
|
|
||||||
fetchDeployments();
|
fetchDeployments();
|
||||||
|
fetchProductionBranchDomains();
|
||||||
const interval = setInterval(() => {
|
}, []);
|
||||||
fetchDeployments();
|
|
||||||
}, FETCH_DEPLOYMENTS_INTERVAL);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
}, [fetchDeployments, fetchProductionBranchDomains]);
|
|
||||||
|
|
||||||
const currentDeployment = useMemo(() => {
|
const currentDeployment = useMemo(() => {
|
||||||
return deployments.find((deployment) => {
|
return deployments.find((deployment) => {
|
||||||
@@ -75,10 +66,8 @@ const DeploymentsTabPanel = () => {
|
|||||||
|
|
||||||
const dateMatch =
|
const dateMatch =
|
||||||
!filterValue.updateAtRange ||
|
!filterValue.updateAtRange ||
|
||||||
(new Date(Number(deployment.createdAt)) >=
|
(new Date(deployment.updatedAt) >= filterValue.updateAtRange!.from! &&
|
||||||
filterValue.updateAtRange!.from! &&
|
new Date(deployment.updatedAt) <= filterValue.updateAtRange!.to!);
|
||||||
new Date(Number(deployment.createdAt)) <=
|
|
||||||
filterValue.updateAtRange!.to!);
|
|
||||||
|
|
||||||
return branchMatch && statusMatch && dateMatch;
|
return branchMatch && statusMatch && dateMatch;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Domain, DomainStatus } from 'gql-client';
|
import { Domain, DomainStatus } from 'gql-client';
|
||||||
import { Link, useNavigate, useOutletContext } from 'react-router-dom';
|
import { useNavigate, useOutletContext } from 'react-router-dom';
|
||||||
import { RequestError } from 'octokit';
|
import { RequestError } from 'octokit';
|
||||||
|
|
||||||
import {
|
import { Typography, Button, Chip, Avatar } from '@material-tailwind/react';
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
Chip,
|
|
||||||
Avatar,
|
|
||||||
Tooltip,
|
|
||||||
} from '@material-tailwind/react';
|
|
||||||
|
|
||||||
import ActivityCard from '../../../../components/projects/project/ActivityCard';
|
import ActivityCard from '../../../../components/projects/project/ActivityCard';
|
||||||
import { relativeTimeMs } from '../../../../utils/time';
|
import { relativeTimeMs } from '../../../../utils/time';
|
||||||
import { useOctokit } from '../../../../context/OctokitContext';
|
import { useOctokit } from '../../../../context/OctokitContext';
|
||||||
import { GitCommitWithBranch, OutletContextType } from '../../../../types';
|
import { GitCommitWithBranch, OutletContextType } from '../../../../types';
|
||||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||||
import { formatAddress } from '../../../../utils/format';
|
|
||||||
|
|
||||||
const COMMITS_PER_PAGE = 4;
|
const COMMITS_PER_PAGE = 4;
|
||||||
|
|
||||||
@@ -31,6 +24,10 @@ const OverviewTabPanel = () => {
|
|||||||
const { project } = useOutletContext<OutletContextType>();
|
const { project } = useOutletContext<OutletContextType>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!octokit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Save repo commits in DB and avoid using GitHub API in frontend
|
// TODO: Save repo commits in DB and avoid using GitHub API in frontend
|
||||||
// TODO: Figure out fetching latest commits for all branches
|
// TODO: Figure out fetching latest commits for all branches
|
||||||
const fetchRepoActivity = async () => {
|
const fetchRepoActivity = async () => {
|
||||||
@@ -129,9 +126,9 @@ const OverviewTabPanel = () => {
|
|||||||
color="green"
|
color="green"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center">
|
<div className="flex justify-between items-center w-full m-2">
|
||||||
<Chip
|
<Chip
|
||||||
className="normal-case inline font-normal mx-2"
|
className="normal-case inline font-normal"
|
||||||
size="sm"
|
size="sm"
|
||||||
value="Not connected"
|
value="Not connected"
|
||||||
icon="^"
|
icon="^"
|
||||||
@@ -164,9 +161,7 @@ const OverviewTabPanel = () => {
|
|||||||
<p>^ Created</p>
|
<p>^ Created</p>
|
||||||
<p>
|
<p>
|
||||||
{relativeTimeMs(project.deployments[0].createdAt)} by ^{' '}
|
{relativeTimeMs(project.deployments[0].createdAt)} by ^{' '}
|
||||||
<Tooltip content={project.deployments[0].createdBy.name}>
|
{project.deployments[0].createdBy.name}
|
||||||
{formatAddress(project.deployments[0].createdBy.name ?? '')}
|
|
||||||
</Tooltip>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -183,11 +178,7 @@ const OverviewTabPanel = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
{activities.map((activity, index) => {
|
{activities.map((activity, index) => {
|
||||||
return (
|
return <ActivityCard activity={activity} key={index} />;
|
||||||
<Link to={activity.html_url} target="_blank" key={index}>
|
|
||||||
<ActivityCard activity={activity} />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { RequestError } from 'octokit';
|
import React, { useEffect, useState } from 'react';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { Link, useOutletContext } from 'react-router-dom';
|
import { Link, useOutletContext } from 'react-router-dom';
|
||||||
import { Domain } from 'gql-client';
|
import { Domain } from 'gql-client';
|
||||||
|
|
||||||
@@ -7,41 +6,14 @@ import { Button, Typography } from '@material-tailwind/react';
|
|||||||
|
|
||||||
import DomainCard from '../../../../../components/projects/project/settings/DomainCard';
|
import DomainCard from '../../../../../components/projects/project/settings/DomainCard';
|
||||||
import { useGQLClient } from '../../../../../context/GQLClientContext';
|
import { useGQLClient } from '../../../../../context/GQLClientContext';
|
||||||
|
import repositories from '../../../../../assets/repositories.json';
|
||||||
import { OutletContextType } from '../../../../../types';
|
import { OutletContextType } from '../../../../../types';
|
||||||
import { useOctokit } from '../../../../../context/OctokitContext';
|
|
||||||
|
|
||||||
const Domains = () => {
|
const Domains = () => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
const { octokit } = useOctokit();
|
|
||||||
const { project } = useOutletContext<OutletContextType>();
|
const { project } = useOutletContext<OutletContextType>();
|
||||||
|
|
||||||
const [domains, setDomains] = useState<Domain[]>([]);
|
const [domains, setDomains] = useState<Domain[]>([]);
|
||||||
const [branches, setBranches] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const fetchBranches = useCallback(async () => {
|
|
||||||
const [owner, repo] = project.repository.split('/');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await octokit.rest.repos.listBranches({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
});
|
|
||||||
|
|
||||||
const branches = result.data.map((repo) => repo.name);
|
|
||||||
|
|
||||||
setBranches(branches);
|
|
||||||
} catch (err) {
|
|
||||||
if (!(err instanceof RequestError && err.status === 404)) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchBranches();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchDomains = async () => {
|
const fetchDomains = async () => {
|
||||||
if (project === undefined) {
|
if (project === undefined) {
|
||||||
@@ -74,7 +46,7 @@ const Domains = () => {
|
|||||||
domain={domain}
|
domain={domain}
|
||||||
key={domain.id}
|
key={domain.id}
|
||||||
// TODO: Use github API for getting linked repository
|
// TODO: Use github API for getting linked repository
|
||||||
branches={branches}
|
repo={repositories[0]!}
|
||||||
project={project}
|
project={project}
|
||||||
onUpdate={fetchDomains}
|
onUpdate={fetchDomains}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -171,8 +171,7 @@ export const EnvironmentVariablesTabPanel = () => {
|
|||||||
>
|
>
|
||||||
+ Add variable
|
+ Add variable
|
||||||
</Button>
|
</Button>
|
||||||
{/* TODO: Implement import environment varible functionality */}
|
<Button variant="outlined" size="sm">
|
||||||
<Button variant="outlined" size="sm" disabled>
|
|
||||||
^ Import .env
|
^ Import .env
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,15 @@ export interface GitOrgDetails {
|
|||||||
avatar_url: string;
|
avatar_url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Use GitRepositoryDetails
|
||||||
|
export interface RepositoryDetails {
|
||||||
|
title: string;
|
||||||
|
updatedAt: string;
|
||||||
|
user: string;
|
||||||
|
private: boolean;
|
||||||
|
branch: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface GitRepositoryDetails {
|
export interface GitRepositoryDetails {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -21,9 +30,6 @@ export interface GitBranchDetails {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GitCommitWithBranch {
|
export interface GitCommitWithBranch {
|
||||||
author: {
|
|
||||||
avatar_url: string;
|
|
||||||
} | null;
|
|
||||||
branch: GitBranchDetails;
|
branch: GitBranchDetails;
|
||||||
commit: {
|
commit: {
|
||||||
author: {
|
author: {
|
||||||
@@ -32,7 +38,6 @@ export interface GitCommitWithBranch {
|
|||||||
} | null;
|
} | null;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
html_url: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GitSelect {
|
export enum GitSelect {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Children, cloneElement, isValidElement } from 'react';
|
||||||
|
import type { Attributes, ReactElement, ReactNode } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones an icon element with optional additional props.
|
||||||
|
* @param {ReactNode} icon - The icon element to clone.
|
||||||
|
* @param {Attributes & P} [props] - Additional props to apply to the cloned icon.
|
||||||
|
* @returns {ReactNode} - The cloned icon element with the applied props.
|
||||||
|
*/
|
||||||
|
export function cloneIcon<P extends object>(
|
||||||
|
icon: ReactNode,
|
||||||
|
props?: Attributes & P,
|
||||||
|
): ReactNode {
|
||||||
|
return Children.map(icon, (child) =>
|
||||||
|
isValidElement(child) ? cloneElement(child as ReactElement, props) : child,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export const formatAddress = (address: string) => {
|
|
||||||
if (address.startsWith('0x') && address.length > 10) {
|
|
||||||
return address.slice(0, 6) + '..' + address.slice(-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
return address;
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,149 @@ export default withMT({
|
|||||||
'../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}',
|
'../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}',
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
emerald: {
|
||||||
|
100: '#d1fae5',
|
||||||
|
200: '#a9f1d0',
|
||||||
|
300: '#6ee7b7',
|
||||||
|
400: '#34d399',
|
||||||
|
50: '#ecfdf5',
|
||||||
|
500: '#10b981',
|
||||||
|
600: '#059669',
|
||||||
|
700: '#047857',
|
||||||
|
800: '#065f46',
|
||||||
|
900: '#064e3b',
|
||||||
|
},
|
||||||
|
gray: {
|
||||||
|
0: '#ffffff',
|
||||||
|
100: '#f1f5f9',
|
||||||
|
200: '#e2e9f0',
|
||||||
|
300: '#cbd6e1',
|
||||||
|
400: '#94a7b8',
|
||||||
|
50: '#f8fafc',
|
||||||
|
500: '#60788f',
|
||||||
|
600: '#475969',
|
||||||
|
700: '#334555',
|
||||||
|
800: '#1b2d3e',
|
||||||
|
900: '#0b1d2e',
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
100: '#ffedd5',
|
||||||
|
200: '#fed7aa',
|
||||||
|
300: '#fdba74',
|
||||||
|
400: '#fb923c',
|
||||||
|
50: '#fff7ed',
|
||||||
|
500: '#f97316',
|
||||||
|
600: '#ea580c',
|
||||||
|
700: '#c2410c',
|
||||||
|
800: '#9a3412',
|
||||||
|
900: '#7c2d12',
|
||||||
|
},
|
||||||
|
rose: {
|
||||||
|
100: '#ffe4e6',
|
||||||
|
200: '#fecdd3',
|
||||||
|
300: '#fda4af',
|
||||||
|
400: '#fb7185',
|
||||||
|
50: '#fff1f2',
|
||||||
|
500: '#f43f5e',
|
||||||
|
600: '#e11d48',
|
||||||
|
700: '#be123c',
|
||||||
|
800: '#9f1239',
|
||||||
|
900: '#881337',
|
||||||
|
},
|
||||||
|
snowball: {
|
||||||
|
100: '#e1f1fe',
|
||||||
|
200: '#ddeefd',
|
||||||
|
300: '#cfe6fc',
|
||||||
|
400: '#74bafb',
|
||||||
|
50: '#ecf6fe',
|
||||||
|
500: '#47a4fa',
|
||||||
|
600: '#0f86f5',
|
||||||
|
700: '#0977dc',
|
||||||
|
800: '#075185',
|
||||||
|
900: '#0a3a5c',
|
||||||
|
},
|
||||||
|
base: {
|
||||||
|
bg: '#ffffff',
|
||||||
|
'bg-alternate': '#f8fafc',
|
||||||
|
'bg-emphasized': '#f1f5f9',
|
||||||
|
'bg-emphasized-danger': '#fff1f2',
|
||||||
|
'bg-emphasized-info': '#ecf6fe',
|
||||||
|
'bg-emphasized-success': '#ecfdf5',
|
||||||
|
'bg-emphasized-warning': '#fff7ed',
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
active: '#0f86f5',
|
||||||
|
danger: '#e11d48',
|
||||||
|
'danger-light': '#ffe4e6',
|
||||||
|
'info-light': '#ddeefd',
|
||||||
|
interactive: '#082f561a',
|
||||||
|
'interactive-hovered': '#082f5624',
|
||||||
|
separator: '#082f560f',
|
||||||
|
'success-light': '#d1fae5',
|
||||||
|
'warning-light': '#ffedd5',
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
danger: '#e11d48',
|
||||||
|
'danger-hovered': '#be123c',
|
||||||
|
disabled: '#e2e9f0',
|
||||||
|
'disabled-active': '#74bafb',
|
||||||
|
elevated: '#ffffff',
|
||||||
|
inset: '#e2e9f0',
|
||||||
|
'inset-hovered': '#cbd6e1',
|
||||||
|
primary: '#0f86f5',
|
||||||
|
'primary-hovered': '#0977dc',
|
||||||
|
secondary: '#ddeefd',
|
||||||
|
'secondary-hovered': '#cfe6fc',
|
||||||
|
tertiary: '#ffffff',
|
||||||
|
'tertiary-hovered': '#f8fafc',
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
danger: '#e11d48',
|
||||||
|
disabled: '#94a7b8',
|
||||||
|
'high-em': '#0b1d2e',
|
||||||
|
info: '#0f86f5',
|
||||||
|
link: '#0f86f5',
|
||||||
|
'link-hovered': '#0977dc',
|
||||||
|
'low-em': '#60788f',
|
||||||
|
'mid-em': '#475969',
|
||||||
|
'on-danger': '#ffffff',
|
||||||
|
'on-disabled': '#60788f',
|
||||||
|
'on-disabled-active': '#0a3a5c',
|
||||||
|
'on-emphasized-danger': '#9f1239',
|
||||||
|
'on-emphasized-info': '#0a3a5c',
|
||||||
|
'on-emphasized-success': '#065f46',
|
||||||
|
'on-emphasized-warning': '#9a3412',
|
||||||
|
'on-high-contrast': '#ffffff',
|
||||||
|
'on-primary': '#ffffff',
|
||||||
|
'on-secondary': '#0977dc',
|
||||||
|
'on-secondary-tinted': '#075185',
|
||||||
|
'on-tertiary': '#1b2d3e',
|
||||||
|
success: '#059669',
|
||||||
|
warning: '#ea580c',
|
||||||
|
},
|
||||||
|
surface: {
|
||||||
|
card: '#ffffff',
|
||||||
|
'card-hovered': '#f8fafc',
|
||||||
|
floating: '#ffffff',
|
||||||
|
'floating-hovered': '#f1f5f9',
|
||||||
|
'high-contrast': '#0b1d2e',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
button:
|
||||||
|
'inset 0px 0px 4px rgba(255, 255, 255, 0.25), inset 0px -2px 0px rgba(0, 0, 0, 0.04)',
|
||||||
|
},
|
||||||
|
spacing: {
|
||||||
|
2.5: '0.625rem',
|
||||||
|
3.25: '0.8125rem',
|
||||||
|
3.5: '0.875rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
|
"baseUrl": "src"
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ export class GQLClient {
|
|||||||
this.client = new ApolloClient({
|
this.client = new ApolloClient({
|
||||||
uri: config.gqlEndpoint,
|
uri: config.gqlEndpoint,
|
||||||
cache: new InMemoryCache(),
|
cache: new InMemoryCache(),
|
||||||
defaultOptions,
|
defaultOptions
|
||||||
credentials: 'include'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,18 +310,6 @@ export class GQLClient {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticateGit (type: types.GitType, code: string): Promise<types.AuthenticateGitResponse> {
|
|
||||||
const { data } = await this.client.mutate({
|
|
||||||
mutation: mutations.authenticateGit,
|
|
||||||
variables: {
|
|
||||||
type,
|
|
||||||
code
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async unauthenticateGithub (): Promise<types.UnauthenticateGitHubResponse> {
|
async unauthenticateGithub (): Promise<types.UnauthenticateGitHubResponse> {
|
||||||
const { data } = await this.client.mutate({
|
const { data } = await this.client.mutate({
|
||||||
mutation: mutations.unauthenticateGitHub
|
mutation: mutations.unauthenticateGitHub
|
||||||
|
|||||||
@@ -95,13 +95,6 @@ mutation ($code: String!) {
|
|||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
export const authenticateGit = gql`
|
|
||||||
mutation ($type: GitType!, $code: String!) {
|
|
||||||
authenticateGit(type: $type, code: $code) {
|
|
||||||
token
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
|
|
||||||
export const unauthenticateGitHub = gql`
|
export const unauthenticateGitHub = gql`
|
||||||
mutation {
|
mutation {
|
||||||
unauthenticateGitHub
|
unauthenticateGitHub
|
||||||
|
|||||||