Compare commits

..
Author SHA1 Message Date
Vivian Phung f5c02f7230 correct suppressRefError 2024-05-22 19:01:49 +00:00
112 changed files with 1621 additions and 4582 deletions
-29
View File
@@ -1,29 +0,0 @@
name: Lint
on:
pull_request:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v2
- name: Download yarn
run: |
curl -fsSL -o /usr/local/bin/yarn https://github.com/yarnpkg/yarn/releases/download/v1.22.21/yarn-1.22.21.js
chmod +x /usr/local/bin/yarn
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: yarn
- name: Build libs
run: yarn workspace gql-client run build
- name: Linter check
run: yarn lint
+1 -4
View File
@@ -6,7 +6,4 @@ yarn-error.log
packages/backend/environments/local.toml
packages/backend/dev/
packages/frontend/dist/
# ignore all .DS_Store files
**/.DS_Store
packages/frontend/dist/
+210 -13
View File
@@ -1,23 +1,220 @@
# snowballtools-base
This is a [yarn workspace](https://yarnpkg.com/features/workspaces) monorepo for the dashboard.
## Setup
## Getting Started
- Clone the `snowballtools-base` repo
### Install dependencies
```bash
git clone git@github.com:snowball-tools/snowballtools-base.git
cd snowballtools-base
```
In the root of the project, run:
- Install dependencies in root
```zsh
yarn
```
```bash
yarn
```
### Build backend
- Build packages
```zsh
yarn build --ignore frontend
```
```bash
yarn build --ignore frontend
```
### Environment variables, running the development server, and deployment
Let us assume the following domains for backend and frontend
- Backend server: `api.snowballtools.com`
- Frontend app: `dashboard.snowballtools.com`
Follow the instructions in the README.md files of the [backend](packages/backend/README.md) and [frontend](packages/frontend/README.md) packages.
- Configuration Files
- For Backend:
```bash
cp packages/backend/environments/local.toml.example packages/backend/environments/local.toml
```
- Production
- Update the following in backend [config file](packages/backend/environments/local.toml)
```toml
[server]
...
[server.session]
# Secret should be changed to a different random string
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7sidnc382"
# Set URL of the frontend app
appOriginUrl = "https://dashboard.snowballtools.com"
# Set to true for session cookies to work behind proxy
trustProxy = true
# Set empty domain when using secure connection
domain = ""
```
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
- Homepage URL: `https://dashboard.snowballtools.com`
- Authorization callback URL: `https://dashboard.snowballtools.com/organization/projects/create`
- Generate a new client secret after app is created
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
[gitHub]
webhookUrl = "https://api.snowballtools.com"
```
- Let us assume domain for Laconicd to be `api.laconic.com` and set the following in backend [config file](packages/backend/environments/local.toml)
```toml
[registryConfig]
fetchDeploymentRecordDelay = 5000
# Use actual port for REST endpoint
restEndpoint = "http://api.laconic.com:1317"
# Use actual port for GQL endpoint
gqlEndpoint = "http://api.laconic.com:9473/api"
# Set private key of account to be used in Laconicd
privateKey = "0wtu92cd4f1y791ezpjwgzzazni4dmd3q3mzqc3t6i6r9v06ji784tey6hwmnn69"
# Set Bond ID to be used for publishing records
bondId = "8xk8c2pb61kajwixpm223zvptr2x2ncajq0vd998p6aqhvqqep2reu6pik245epf"
chainId = "laconic_9000-1"
# Set authority that is existing in the chain
authority = "laconic"
[registryConfig.fee]
amount = "200000"
denom = "aphoton"
gas = "750000"
```
- Development
- Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- [OAuth App Creation](https://github.com/organizations/<org>/settings/applications/new)
- Homepage URL: `http://localhost:3000`
- Authorization callback URL: `http://localhost:3000/organization/projects/create`
- Generate a new client secret after app is created
- Setup Laconicd
- Run the laconicd stack following this [doc](https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/laconicd-with-console.md)
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
```bash
laconic-so deployment --dir laconic-loaded-deployment exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
```
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
- Run the script to create bond, reserve the authority and set authority bond
```bash
yarn test:registry:init
# snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms
```
- Get the bond id and set `registryConfig.bondId` in backend [config file](packages/backend/environments/local.toml)
- Setup ngrok for GitHub webhooks
- [ngrok getting started](https://ngrok.com/docs/getting-started/)
- Start ngrok and point to backend server endpoint
```bash
ngrok http http://localhost:8000
```
- Look for the forwarding URL in ngrok
```bash
Forwarding https://19c1-61-95-158-116.ngrok-free.app -> http://localhost:8000
```
- Set `gitHub.webhookUrl` in backend [config file](packages/backend/environments/local.toml)
```toml
[gitHub]
webhookUrl = "https://19c1-61-95-158-116.ngrok-free.app"
```
- For Frontend:
```bash
cp packages/frontend/.env.example packages/frontend/.env
```
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
```env
VITE_GITHUB_CLIENT_ID = <CLIENT_ID>
```
- Set `VITE_GITHUB_PWA_TEMPLATE_REPO` and `VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
```env
VITE_GITHUB_PWA_TEMPLATE_REPO = 'cerc-io/test-progressive-web-app' # Set actual owner/name of the template repo that will be used for creating new repo
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'cerc-io/image-upload-pwa-example' # Set actual owner/name of the template repo that will be used for creating new repo
```
- Production
- Set the following values in [.env](packages/frontend/.env) file
```env
VITE_SERVER_URL = 'https://api.snowballtools.com' # Backend server endpoint
```
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
- Create a project and add information to use wallet connect SDK
- Add project name and select project type as `App`
- Set project home page URL to `https://dashboard.snowballtools.com`
- On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
```env
VITE_WALLET_CONNECT_ID = <PROJECT_ID>
```
- Build the React application
```bash
yarn build
```
- Use a web server for hosting static built files
```bash
python3 -m http.server -d build 3000
```
- Development
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
```env
VITE_SERVER_URL = 'http://localhost:8000'
```
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID.
- Create a project and add information to use wallet connect SDK
- Add project name and select project type as `App`
- Project home page URL is not required to be set
- On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
```env
VITE_WALLET_CONNECT_ID = <Project_ID>
```
- The React application will be running in `http://localhost:3000/`
## Development
- Start the backend server
```bash
yarn start:backend
```
- Start the frontend
```bash
yarn start
```
+1 -76
View File
@@ -1,76 +1 @@
# backend
This backend is a [node.js](https://nodejs.org/) [express.js](https://expressjs.com/) [apollo server](https://www.apollographql.com/docs/apollo-server/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
## Getting Started
### Install dependencies
In the root of the project, run:
```zsh
yarn
```
### Build backend
```zsh
yarn build --ignore frontend
```
### Environment variables
#### Local
Copy the `environments/local.toml.example` file to `environments/local.toml`:
```zsh
cp environments/local.toml.example environments/local.toml
```
#### Staging environment variables
In the deployment repository, update staging [staging/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/staging/configmaps/config/prod.toml)
#### Production environment variables
In the deployment repository, update production [production/configmaps/config/prod.toml](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments/src/commit/318c2bc09f334dca79c3501838512749f9431bf1/deployments/production/configmaps/config/prod.toml)
### Run development server
```zsh
yarn start
```
## Deployment
Clone the [deployer repository](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments):
```zsh
git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git
```
### Staging
```zsh
echo trigger >> .gitea/workflows/triggers/staging-deploy
git commit -a -m "Deploy v0.0.8" # replace with version number
git push
```
### Production
```zsh
echo trigger >> .gitea/workflows/triggers/production-deploy
git commit -a -m "Deploy v0.0.8" # replace with version number
git push
```
### Deployment status
Dumb for now
- [Staging](https://snowballtools-base-api.staging.apps.snowballtools.com/staging/version)
- [Production](https://snowballtools-base-api.apps.snowballtools.com/staging/version)
Update version number manually in [routes/staging.ts](/packages/backend/src/routes/staging.ts)
# Backend for Snowball Tools
@@ -3,7 +3,7 @@
port = 8000
gqlPath = "/graphql"
[server.session]
secret = ""
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi"
appOriginUrl = "http://localhost:3000"
trustProxy = false
domain = "localhost"
@@ -17,19 +17,8 @@
clientId = ""
clientSecret = ""
[google]
clientId = ""
clientSecret = ""
[turnkey]
apiBaseUrl = "https://api.turnkey.com"
apiPrivateKey = ""
apiPublicKey = ""
defaultOrganizationId = ""
[registryConfig]
fetchDeploymentRecordDelay = 5000
checkAuctionStatusDelay = 5000
restEndpoint = "http://localhost:1317"
gqlEndpoint = "http://localhost:9473/api"
chainId = "laconic_9000-1"
@@ -37,16 +26,9 @@
bondId = ""
authority = ""
[registryConfig.fee]
gas = ""
fees = ""
gasPrice = "1alnt"
[auction]
commitFee = "1000"
commitsDuration = "60s"
revealFee = "1000"
revealsDuration = "60s"
denom = "alnt"
amount = "200000"
denom = "aphoton"
gas = "750000"
[misc]
projectDomain = "apps.snowballtools.com"
+1 -2
View File
@@ -4,10 +4,10 @@
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"@cerc-io/registry-sdk": "^0.2.11",
"@graphql-tools/schema": "^10.0.2",
"@graphql-tools/utils": "^10.0.12",
"@octokit/oauth-app": "^6.1.0",
"@snowballtools/laconic-sdk": "^0.1.17",
"@turnkey/sdk-server": "^0.1.0",
"@types/debug": "^4.1.5",
"@types/express": "^4.17.21",
@@ -19,7 +19,6 @@
"cors": "^2.8.5",
"debug": "^4.3.1",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"express-session": "^1.18.0",
"fs-extra": "^11.2.0",
"graphql": "^16.8.1",
+2 -12
View File
@@ -34,23 +34,14 @@ export interface RegistryConfig {
privateKey: string;
bondId: string;
fetchDeploymentRecordDelay: number;
checkAuctionStatusDelay: number;
authority: string;
fee: {
amount: string;
denom: string;
gas: string;
fees: string;
gasPrice: string;
};
}
export interface AuctionConfig {
commitFee: string;
commitsDuration: string;
revealFee: string;
revealsDuration: string;
denom: string;
}
export interface MiscConfig {
projectDomain: string;
}
@@ -60,7 +51,6 @@ export interface Config {
database: DatabaseConfig;
gitHub: GitHubConfig;
registryConfig: RegistryConfig;
auction: AuctionConfig;
misc: MiscConfig;
turnkey: {
apiBaseUrl: string;
+2
View File
@@ -465,6 +465,8 @@ export class Database {
id: organizationId
});
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
return projectRepository.save(newProject);
}
+3 -11
View File
@@ -33,8 +33,6 @@ export interface ApplicationDeploymentRequest {
version: string;
name: string;
application: string;
lrn?: string;
auction?: string;
config: string;
meta: string;
}
@@ -114,22 +112,19 @@ export class Deployment {
@Column('simple-json', { nullable: true })
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
@Column('varchar', { nullable: true })
applicationDeploymentRemovalRequestId!: string | null;
@Column('simple-json', { nullable: true })
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null;
@Column('varchar', { nullable: true })
applicationDeploymentRemovalRecordId!: string | null;
@Column('simple-json', { nullable: true })
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
@Column('varchar')
deployerLrn!: string;
@Column({
enum: Environment
})
@@ -138,9 +133,6 @@ export class Deployment {
@Column('boolean', { default: false })
isCurrent!: boolean;
@Column('varchar', { nullable: true })
baseDomain!: string | null;
@Column({
enum: DeploymentStatus
})
@@ -155,7 +147,7 @@ export class Deployment {
@UpdateDateColumn()
updatedAt!: Date;
@DeleteDateColumn()
deletedAt!: Date | null;
}
+2 -8
View File
@@ -46,12 +46,6 @@ export class Project {
@Column('text', { default: '' })
description!: string;
@Column('varchar', { nullable: true })
auctionId!: string | null;
@Column({ type: 'simple-array', nullable: true })
deployerLrns!: string[] | null;
// TODO: Compute template & framework in import repository
@Column('varchar', { nullable: true })
template!: string | null;
@@ -67,8 +61,8 @@ export class Project {
@Column('varchar')
icon!: string;
@Column({ type: 'simple-array', nullable: true })
baseDomains!: string[] | null;
@Column('varchar')
subDomain!: string;
@CreateDateColumn()
createdAt!: Date;
-1
View File
@@ -1,4 +1,3 @@
import 'express-async-errors';
import 'reflect-metadata';
import debug from 'debug';
import fs from 'fs';
+60 -202
View File
@@ -1,11 +1,9 @@
import assert from 'assert';
import debug from 'debug';
import { DateTime } from 'luxon';
import { Octokit } from 'octokit';
import assert from 'assert';
import { inc as semverInc } from 'semver';
import { DeepPartial } from 'typeorm';
import { DateTime } from 'luxon';
import { Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk';
import { Registry as LaconicRegistry } from '@snowballtools/laconic-sdk';
import { RegistryConfig } from './config';
import {
@@ -14,53 +12,49 @@ import {
ApplicationDeploymentRequest,
ApplicationDeploymentRemovalRequest
} from './entity/Deployment';
import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionData } from './types';
import { getConfig, getRepoDetails, sleep } from './utils';
import { Auction } from '@cerc-io/registry-sdk/dist/proto/cerc/auction/v1/auction';
import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types';
import { sleep } from './utils';
const log = debug('snowball:registry');
const APP_RECORD_TYPE = 'ApplicationRecord';
const APP_DEPLOYMENT_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction';
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
const SLEEP_DURATION = 1000;
// TODO: Move registry code to registry-sdk/watcher-ts
// TODO: Move registry code to laconic-sdk/watcher-ts
export class Registry {
private registry: LaconicRegistry;
private registryConfig: RegistryConfig;
constructor(registryConfig: RegistryConfig) {
constructor (registryConfig: RegistryConfig) {
this.registryConfig = registryConfig;
const gasPrice = getGasPrice(registryConfig.fee.gasPrice);
this.registry = new LaconicRegistry(
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
{ chainId: registryConfig.chainId, gasPrice }
registryConfig.chainId
);
}
async createApplicationRecord({
octokit,
repository,
async createApplicationRecord ({
appName,
packageJSON,
commitHash,
appType,
repoUrl
}: {
octokit: Octokit
repository: string;
appName: string;
packageJSON: PackageJSON;
commitHash: string;
appType: string;
repoUrl: string;
}): Promise<{
applicationRecordId: string;
applicationRecordData: ApplicationRecord;
}> {
const { repo, repoUrl, packageJSON } = await getRepoDetails(octokit, repository, commitHash)
// Use registry-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
// Fetch previous records
const records = await this.registry.queryRecords(
@@ -93,7 +87,7 @@ export class Registry {
repository_ref: commitHash,
repository: [repoUrl],
app_type: appType,
name: repo,
name: appName,
...(packageJSON.description && { description: packageJSON.description }),
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
...(packageJSON.license && { license: packageJSON.license }),
@@ -106,145 +100,68 @@ export class Registry {
...(packageJSON.version && { app_version: packageJSON.version })
};
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
const result = await this.registry.setRecord(
{
privateKey: this.registryConfig.privateKey,
record: applicationRecord,
bondId: this.registryConfig.bondId
},
this.registryConfig.privateKey,
fee
'',
this.registryConfig.fee
);
log(`Published application record ${result.id}`);
log('Application record data:', applicationRecord);
// TODO: Discuss computation of LRN
const lrn = this.getLrn(repo);
log(`Setting name: ${lrn} for record ID: ${result.id}`);
// TODO: Discuss computation of CRN
const crn = this.getCrn(appName);
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
await sleep(SLEEP_DURATION);
await this.registry.setName(
{
cid: result.id,
lrn
},
{ cid: result.data.id, crn },
this.registryConfig.privateKey,
fee
this.registryConfig.fee
);
await sleep(SLEEP_DURATION);
await this.registry.setName(
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
this.registryConfig.privateKey,
this.registryConfig.fee
);
await sleep(SLEEP_DURATION);
await this.registry.setName(
{
cid: result.id,
lrn: `${lrn}@${applicationRecord.app_version}`
cid: result.data.id,
crn: `${crn}@${applicationRecord.repository_ref}`
},
this.registryConfig.privateKey,
fee
);
await sleep(SLEEP_DURATION);
await this.registry.setName(
{
cid: result.id,
lrn: `${lrn}@${applicationRecord.repository_ref}`
},
this.registryConfig.privateKey,
fee
this.registryConfig.fee
);
return {
applicationRecordId: result.id,
applicationRecordId: result.data.id,
applicationRecordData: applicationRecord
};
}
async createApplicationDeploymentAuction(
appName: string,
octokit: Octokit,
auctionData: AuctionData,
data: DeepPartial<Deployment>,
): Promise<{
applicationDeploymentAuctionId: string;
}> {
assert(data.project?.repository, 'Project repository not found');
await this.createApplicationRecord({
octokit,
repository: data.project.repository,
appType: data.project!.template!,
commitHash: data.commitHash!,
});
const lrn = this.getLrn(appName);
const config = await getConfig();
const auctionConfig = config.auction;
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
const auctionResult = await this.registry.createProviderAuction(
{
commitFee: auctionConfig.commitFee,
commitsDuration: auctionConfig.commitsDuration,
revealFee: auctionConfig.revealFee,
revealsDuration: auctionConfig.revealsDuration,
denom: auctionConfig.denom,
maxPrice: auctionData.maxPrice,
numProviders: auctionData.numProviders,
},
this.registryConfig.privateKey,
fee
)
if (!auctionResult.auction) {
throw new Error('Error creating auction');
}
// Create record of type applicationDeploymentAuction and publish
const applicationDeploymentAuction = {
application: lrn,
auction: auctionResult.auction.id,
type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE,
};
const result = await this.registry.setRecord(
{
privateKey: this.registryConfig.privateKey,
record: applicationDeploymentAuction,
bondId: this.registryConfig.bondId
},
this.registryConfig.privateKey,
fee
);
log(`Application deployment auction created: ${auctionResult.auction.id}`);
log(`Application deployment auction record published: ${result.id}`);
log('Application deployment auction data:', applicationDeploymentAuction);
return {
applicationDeploymentAuctionId: auctionResult.auction.id,
};
}
async createApplicationDeploymentRequest(data: {
async createApplicationDeploymentRequest (data: {
deployment: Deployment,
appName: string,
repository: string,
auctionId?: string,
lrn: string,
environmentVariables: { [key: string]: string },
dns: string,
}): Promise<{
applicationDeploymentRequestId: string;
applicationDeploymentRequestData: ApplicationDeploymentRequest;
}> {
const lrn = this.getLrn(data.appName);
const records = await this.registry.resolveNames([lrn]);
const crn = this.getCrn(data.appName);
const records = await this.registry.resolveNames([crn]);
const applicationRecord = records[0];
if (!applicationRecord) {
throw new Error(`No record found for ${lrn}`);
throw new Error(`No record found for ${crn}`);
}
// Create record of type ApplicationDeploymentRequest and publish
@@ -252,10 +169,12 @@ export class Registry {
type: APP_DEPLOYMENT_REQUEST_TYPE,
version: '1.0.0',
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
application: `${lrn}@${applicationRecord.attributes.app_version}`,
application: `${crn}@${applicationRecord.attributes.app_version}`,
dns: data.dns,
// TODO: Not set in test-progressive-web-app CI
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
config: JSON.stringify({
env: data.environmentVariables
@@ -266,65 +185,32 @@ export class Registry {
)}`,
repository: data.repository,
repository_ref: data.deployment.commitHash
}),
deployer: data.lrn,
...(data.auctionId && { auction: data.auctionId }),
})
};
await sleep(SLEEP_DURATION);
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
const result = await this.registry.setRecord(
{
privateKey: this.registryConfig.privateKey,
record: applicationDeploymentRequest,
bondId: this.registryConfig.bondId
},
this.registryConfig.privateKey,
fee
'',
this.registryConfig.fee
);
log(`Application deployment request record published: ${result.id}`);
log(`Application deployment request record published: ${result.data.id}`);
log('Application deployment request data:', applicationDeploymentRequest);
return {
applicationDeploymentRequestId: result.id,
applicationDeploymentRequestId: result.data.id,
applicationDeploymentRequestData: applicationDeploymentRequest
};
}
async getAuctionWinningDeployers(
auctionId: string
): Promise<string[]> {
const records = await this.registry.getAuctionsByIds([auctionId]);
const auctionResult = records[0];
let deployerLrns = [];
const { winnerAddresses } = auctionResult;
for (const auctionWinner of winnerAddresses) {
const deployerRecords = await this.registry.queryRecords(
{
paymentAddress: auctionWinner,
},
true
);
for (const record of deployerRecords) {
if (record.names && record.names.length > 0) {
deployerLrns.push(record.names[0]);
break;
}
}
}
return deployerLrns;
}
/**
* Fetch ApplicationDeploymentRecords for deployments
*/
async getDeploymentRecords(
async getDeploymentRecords (
deployments: Deployment[]
): Promise<AppDeploymentRecord[]> {
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
@@ -336,11 +222,11 @@ export class Registry {
true
);
// Filter records with ApplicationDeploymentRequestId ID and Deployment specific URL
// Filter records with ApplicationRecord ID and Deployment specific URL
return records.filter((record: AppDeploymentRecord) =>
deployments.some(
(deployment) =>
deployment.applicationDeploymentRequestId === record.attributes.request &&
deployment.applicationRecordId === record.attributes.application &&
record.attributes.url.includes(deployment.id)
)
);
@@ -349,7 +235,7 @@ export class Registry {
/**
* Fetch ApplicationDeploymentRecords by filter
*/
async getDeploymentRecordsByFilter(filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
return this.registry.queryRecords(
{
type: APP_DEPLOYMENT_RECORD_TYPE,
@@ -362,7 +248,7 @@ export class Registry {
/**
* Fetch ApplicationDeploymentRemovalRecords for deployments
*/
async getDeploymentRemovalRecords(
async getDeploymentRemovalRecords (
deployments: Deployment[]
): Promise<AppDeploymentRemovalRecord[]> {
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
@@ -383,9 +269,8 @@ export class Registry {
);
}
async createApplicationDeploymentRemovalRequest(data: {
async createApplicationDeploymentRemovalRequest (data: {
deploymentId: string;
deployerLrn: string;
}): Promise<{
applicationDeploymentRemovalRequestId: string;
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
@@ -393,57 +278,30 @@ export class Registry {
const applicationDeploymentRemovalRequest = {
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
version: '1.0.0',
deployment: data.deploymentId,
deployer: data.deployerLrn
deployment: data.deploymentId
};
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
const result = await this.registry.setRecord(
{
privateKey: this.registryConfig.privateKey,
record: applicationDeploymentRemovalRequest,
bondId: this.registryConfig.bondId
},
this.registryConfig.privateKey,
fee
'',
this.registryConfig.fee
);
log(`Application deployment removal request record published: ${result.id}`);
log(`Application deployment removal request record published: ${result.data.id}`);
log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
return {
applicationDeploymentRemovalRequestId: result.id,
applicationDeploymentRemovalRequestId: result.data.id,
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
};
}
async getCompletedAuctionIds(auctionIds: (string | null | undefined)[]): Promise<string[] | null> {
const validAuctionIds = auctionIds.filter((id): id is string => id !== null && id !== undefined);
if (!validAuctionIds.length) {
return null;
}
const auctions = await this.registry.getAuctionsByIds(validAuctionIds);
const completedAuctions = auctions
.filter((auction: Auction) => auction.status === 'completed')
.map((auction: Auction) => auction.id);
return completedAuctions;
}
async getRecordsByName(name: string): Promise<any> {
return this.registry.resolveNames([name]);
}
async getAuctionData(auctionId: string): Promise<any> {
return this.registry.getAuctionsByIds([auctionId]);
}
getLrn(appName: string): string {
getCrn (appName: string): string {
assert(this.registryConfig.authority, "Authority doesn't exist");
return `lrn://${this.registryConfig.authority}/applications/${appName}`;
return `crn://${this.registryConfig.authority}/applications/${appName}`;
}
}
+41 -93
View File
@@ -6,7 +6,6 @@ import { Permission } from './entity/ProjectMember';
import { Domain } from './entity/Domain';
import { Project } from './entity/Project';
import { EnvironmentVariable } from './entity/EnvironmentVariable';
import { AddProjectFromTemplateInput, AuctionData } from './types';
const log = debug('snowball:resolver');
@@ -18,7 +17,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
return context.user;
},
organizations: async (_: any, __: any, context: any) => {
organizations: async (_:any, __: any, context: any) => {
return service.getOrganizationsByUserId(context.user);
},
@@ -26,15 +25,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectById(projectId);
},
projectsInOrganization: async (
_: any,
{ organizationSlug }: { organizationSlug: string },
context: any,
) => {
return service.getProjectsInOrganization(
context.user,
organizationSlug,
);
projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
return service.getProjectsInOrganization(context.user, organizationSlug);
},
deployments: async (_: any, { projectId }: { projectId: string }) => {
@@ -43,7 +35,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
environmentVariables: async (
_: any,
{ projectId }: { projectId: string },
{ projectId }: { projectId: string }
) => {
return service.getEnvironmentVariablesByProjectId(projectId);
},
@@ -52,11 +44,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
return service.getProjectMembersByProjectId(projectId);
},
searchProjects: async (
_: any,
{ searchText }: { searchText: string },
context: any,
) => {
searchProjects: async (_: any, { searchText }: { searchText: string }, context: any) => {
return service.searchProjects(context.user, searchText);
},
@@ -64,18 +52,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectId,
filter,
}: { projectId: string; filter?: FindOptionsWhere<Domain> },
filter
}: { projectId: string; filter?: FindOptionsWhere<Domain> }
) => {
return service.getDomainsByProjectId(projectId, filter);
},
getAuctionData: async (
_: any,
{ auctionId }: { auctionId: string },
) => {
return service.getAuctionData(auctionId);
},
}
},
// TODO: Return error in GQL response
@@ -83,13 +64,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
removeProjectMember: async (
_: any,
{ projectMemberId }: { projectMemberId: string },
context: any,
context: any
) => {
try {
return await service.removeProjectMember(
context.user,
projectMemberId,
);
return await service.removeProjectMember(context.user, projectMemberId);
} catch (err) {
log(err);
return false;
@@ -100,13 +78,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectMemberId,
data,
data
}: {
projectMemberId: string;
data: {
permissions: Permission[];
};
},
}
) => {
try {
return await service.updateProjectMember(projectMemberId, data);
@@ -120,14 +98,14 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectId,
data,
data
}: {
projectId: string;
data: {
email: string;
permissions: Permission[];
};
},
}
) => {
try {
return Boolean(await service.addProjectMember(projectId, data));
@@ -141,15 +119,15 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectId,
data,
data
}: {
projectId: string;
data: { environments: string[]; key: string; value: string }[];
},
}
) => {
try {
return Boolean(
await service.addEnvironmentVariables(projectId, data),
await service.addEnvironmentVariables(projectId, data)
);
} catch (err) {
log(err);
@@ -161,16 +139,16 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
environmentVariableId,
data,
data
}: {
environmentVariableId: string;
data: DeepPartial<EnvironmentVariable>;
},
}
) => {
try {
return await service.updateEnvironmentVariable(
environmentVariableId,
data,
data
);
} catch (err) {
log(err);
@@ -180,7 +158,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
removeEnvironmentVariable: async (
_: any,
{ environmentVariableId }: { environmentVariableId: string },
{ environmentVariableId }: { environmentVariableId: string }
) => {
try {
return await service.removeEnvironmentVariable(environmentVariableId);
@@ -193,54 +171,26 @@ export const createResolvers = async (service: Service): Promise<any> => {
updateDeploymentToProd: async (
_: any,
{ deploymentId }: { deploymentId: string },
context: any,
context: any
) => {
try {
return Boolean(
await service.updateDeploymentToProd(context.user, deploymentId),
);
return Boolean(await service.updateDeploymentToProd(context.user, deploymentId));
} catch (err) {
log(err);
return false;
}
},
addProjectFromTemplate: async (
_: any,
{
organizationSlug,
data,
lrn,
auctionData
}: { organizationSlug: string; data: AddProjectFromTemplateInput; lrn: string; auctionData: AuctionData },
context: any,
) => {
try {
return await service.addProjectFromTemplate(
context.user,
organizationSlug,
data,
lrn,
auctionData
);
} catch (err) {
log(err);
throw err;
}
},
addProject: async (
_: any,
{
organizationSlug,
data,
lrn,
auctionData
}: { organizationSlug: string; data: DeepPartial<Project>; lrn: string; auctionData: AuctionData },
context: any,
data
}: { organizationSlug: string; data: DeepPartial<Project> },
context: any
) => {
try {
return await service.addProject(context.user, organizationSlug, data, lrn, auctionData);
return await service.addProject(context.user, organizationSlug, data);
} catch (err) {
log(err);
throw err;
@@ -249,7 +199,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
updateProject: async (
_: any,
{ projectId, data }: { projectId: string; data: DeepPartial<Project> },
{ projectId, data }: { projectId: string; data: DeepPartial<Project> }
) => {
try {
return await service.updateProject(projectId, data);
@@ -262,12 +212,10 @@ export const createResolvers = async (service: Service): Promise<any> => {
redeployToProd: async (
_: any,
{ deploymentId }: { deploymentId: string },
context: any,
context: any
) => {
try {
return Boolean(
await service.redeployToProd(context.user, deploymentId),
);
return Boolean(await service.redeployToProd(context.user, deploymentId));
} catch (err) {
log(err);
return false;
@@ -296,8 +244,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
_: any,
{
projectId,
deploymentId,
}: { deploymentId: string; projectId: string },
deploymentId
}: { deploymentId: string; projectId: string }
) => {
try {
return await service.rollbackDeployment(projectId, deploymentId);
@@ -309,7 +257,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
deleteDeployment: async (
_: any,
{ deploymentId }: { deploymentId: string },
{
deploymentId
}: { deploymentId: string; }
) => {
try {
return await service.deleteDeployment(deploymentId);
@@ -321,7 +271,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
addDomain: async (
_: any,
{ projectId, data }: { projectId: string; data: { name: string } },
{ projectId, data }: { projectId: string; data: { name: string } }
) => {
try {
return Boolean(await service.addDomain(projectId, data));
@@ -333,7 +283,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
updateDomain: async (
_: any,
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> },
{ domainId, data }: { domainId: string; data: DeepPartial<Domain> }
) => {
try {
return await service.updateDomain(domainId, data);
@@ -346,7 +296,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
authenticateGitHub: async (
_: any,
{ code }: { code: string },
context: any,
context: any
) => {
try {
return await service.authenticateGitHub(code, context.user);
@@ -358,14 +308,12 @@ export const createResolvers = async (service: Service): Promise<any> => {
unauthenticateGitHub: async (_: any, __: object, context: any) => {
try {
return service.unauthenticateGitHub(context.user, {
gitHubToken: null,
});
return service.unauthenticateGitHub(context.user, { gitHubToken: null });
} catch (err) {
log(err);
return false;
}
},
},
}
}
};
};
-15
View File
@@ -5,19 +5,6 @@ import { authenticateUser, createUser } from '../turnkey-backend';
const router = Router();
//
// Access Code
//
router.post('/accesscode', async (req, res) => {
console.log('Access Code', req.body);
const { accesscode } = req.body;
if (accesscode === '44444') {
return res.send({ isValid: true });
} else {
return res.sendStatus(204);
}
});
//
// Turnkey
//
@@ -32,7 +19,6 @@ router.get('/registration/:email', async (req, res) => {
});
router.post('/register', async (req, res) => {
console.log('Register', req.body);
const { email, challenge, attestation } = req.body;
const user = await createUser(req.app.get('service'), {
challenge,
@@ -45,7 +31,6 @@ router.post('/register', async (req, res) => {
});
router.post('/authenticate', async (req, res) => {
console.log('Authenticate', req.body);
const { signedWhoamiRequest } = req.body;
const user = await authenticateUser(
req.app.get('service'),
-9
View File
@@ -1,9 +0,0 @@
import { Router } from 'express';
const router = Router();
router.get('/version', async (req, res) => {
return res.send({ version: '0.0.9' });
});
export default router;
+2 -75
View File
@@ -22,13 +22,6 @@ enum DeploymentStatus {
Deleting
}
enum AuctionStatus {
completed
reveal
commit
expired
}
enum DomainStatus {
Live
Pending
@@ -72,8 +65,6 @@ type Project {
repository: String!
prodBranch: String!
description: String
deployerLrns: [String]
auctionId: String
template: String
framework: String
webhooks: [String!]
@@ -83,7 +74,7 @@ type Project {
updatedAt: String!
organization: Organization!
icon: String
baseDomains: [String!]
subDomain: String
}
type ProjectMember {
@@ -103,9 +94,7 @@ type Deployment {
commitMessage: String!
url: String
environment: Environment!
deployerLrn: String
isCurrent: Boolean!
baseDomain: String
status: DeploymentStatus!
createdAt: String!
updatedAt: String!
@@ -141,14 +130,6 @@ input AddEnvironmentVariableInput {
value: String!
}
input AddProjectFromTemplateInput {
templateOwner: String!
templateRepo: String!
owner: String!
name: String!
isPrivate: Boolean!
}
input AddProjectInput {
name: String!
repository: String!
@@ -193,48 +174,6 @@ input FilterDomainsInput {
status: DomainStatus
}
type Fee {
type: String!
quantity: String!
}
type Bid {
auctionId: String!
bidderAddress: String!
status: String!
commitHash: String!
commitTime: String
commitFee: Fee
revealTime: String
revealFee: Fee
bidAmount: Fee
}
type Auction {
id: String!
kind: String!
status: String!
ownerAddress: String!
createTime: String!
commitsEndTime: String!
revealsEndTime: String!
commitFee: Fee!
revealFee: Fee!
minimumBid: Fee
winnerAddresses: [String!]!
winnerBids: [Fee!]
winnerPrice: Fee
maxPrice: Fee
numProviders: Int!
fundsReleased: Boolean!
bids: [Bid!]!
}
input AuctionData {
maxPrice: String,
numProviders: Int,
}
type Query {
user: User!
organizations: [Organization!]
@@ -245,7 +184,6 @@ type Query {
environmentVariables(projectId: String!): [EnvironmentVariable!]
projectMembers(projectId: String!): [ProjectMember!]
searchProjects(searchText: String!): [Project!]
getAuctionData(auctionId: String!): Auction!
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
}
@@ -266,18 +204,7 @@ type Mutation {
): Boolean!
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
updateDeploymentToProd(deploymentId: String!): Boolean!
addProjectFromTemplate(
organizationSlug: String!
data: AddProjectFromTemplateInput
lrn: String
auctionData: AuctionData
): Project!
addProject(
organizationSlug: String!
data: AddProjectInput!
lrn: String
auctionData: AuctionData
): Project!
addProject(organizationSlug: String!, data: AddProjectInput): Project!
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
redeployToProd(deploymentId: String!): Boolean!
deleteProject(projectId: String!): Boolean!
-7
View File
@@ -17,7 +17,6 @@ import { ServerConfig } from './config';
import { DEFAULT_GQL_PATH } from './constants';
import githubRouter from './routes/github';
import authRouter from './routes/auth';
import stagingRouter from './routes/staging';
import { Service } from './service';
const log = debug('snowball:server');
@@ -110,12 +109,6 @@ export const createAndStartServer = async (
app.set('service', service);
app.use('/auth', authRouter);
app.use('/api/github', githubRouter);
app.use('/staging', stagingRouter);
app.use((err: any, req: any, res: any, next: any) => {
console.error(err);
res.status(500).json({ error: err.message });
});
httpServer.listen(port, host, () => {
log(`Server is listening on ${host}:${port}${server.graphqlPath}`);
File diff suppressed because it is too large Load Diff
+1 -36
View File
@@ -29,8 +29,6 @@ export interface GitPushEventPayload {
export interface AppDeploymentRecordAttributes {
application: string;
auction: string;
deployer: string;
dns: string;
meta: string;
name: string;
@@ -43,7 +41,7 @@ export interface AppDeploymentRecordAttributes {
export interface AppDeploymentRemovalRecordAttributes {
deployment: string;
request: string;
type: 'ApplicationDeploymentRemovalRecord';
type: "ApplicationDeploymentRemovalRecord";
version: string;
}
@@ -63,36 +61,3 @@ export interface AppDeploymentRecord extends RegistryRecord {
export interface AppDeploymentRemovalRecord extends RegistryRecord {
attributes: AppDeploymentRemovalRecordAttributes;
}
export interface AddProjectFromTemplateInput {
templateOwner: string;
templateRepo: string;
owner: string;
name: string;
isPrivate: boolean;
}
export interface Auction {
id: string;
kind: string;
status: string;
ownerAddress: string;
createTime: Date;
commitsEndTime: Date;
revealsEndTime: Date;
commitFee: string;
revealFee: string;
minimumBid?: string;
winnerAddresses: string[];
winnerBids?: string[];
winnerPrice?: string;
maxPrice?: string;
numProviders: number;
fundsReleased: boolean;
bids: string[];
}
export interface AuctionData {
maxPrice: string,
numProviders: number,
}
+1 -45
View File
@@ -1,14 +1,10 @@
import assert from 'assert';
import debug from 'debug';
import fs from 'fs-extra';
import { Octokit } from 'octokit';
import path from 'path';
import toml from 'toml';
import debug from 'debug';
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
import { Config } from './config';
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
import { PackageJSON } from './types';
const log = debug('snowball:utils');
@@ -80,43 +76,3 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
export const sleep = async (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));
export const getRepoDetails = async (
octokit: Octokit,
repository: string,
commitHash: string | undefined,
): Promise<{
repo: string;
packageJSON: PackageJSON;
repoUrl: string;
}> => {
const [owner, repo] = repository.split('/');
const { data: packageJSONData } = await octokit.rest.repos.getContent({
owner,
repo,
path: 'package.json',
ref: commitHash,
});
if (!packageJSONData) {
throw new Error('Package.json file not found');
}
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
assert(packageJSON.name, "name field doesn't exist in package.json");
const repoUrl = (
await octokit.rest.repos.get({
owner,
repo,
})
).data.html_url;
return {
repo,
packageJSON,
repoUrl
};
}
+3 -1
View File
@@ -2,6 +2,8 @@ import * as fs from 'fs/promises';
import debug from 'debug';
import { getConfig } from '../src/utils';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
const log = debug('snowball:delete-database');
@@ -11,7 +13,7 @@ const deleteFile = async (filePath: string) => {
};
const main = async () => {
const config = await getConfig();
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
deleteFile(config.database.dbPath);
};
+3 -1
View File
@@ -16,6 +16,8 @@ import {
getEntities,
loadAndSaveData
} from '../src/utils';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
const log = debug('snowball:initialize-database');
@@ -154,7 +156,7 @@ const generateTestData = async (dataSource: DataSource) => {
};
const main = async () => {
const config = await getConfig();
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const isDbPresent = await checkFileExists(config.database.dbPath);
if (!isDbPresent) {
+9 -10
View File
@@ -1,40 +1,39 @@
import debug from 'debug';
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
import { Registry } from '@snowballtools/laconic-sdk';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
import { Config } from '../src/config';
import { getConfig } from '../src/utils';
const log = debug('snowball:initialize-registry');
const DENOM = 'alnt';
const DENOM = 'aphoton';
const BOND_AMOUNT = '1000000000';
async function main () {
const { registryConfig } = await getConfig();
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, {chainId: registryConfig.chainId});
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
const bondId = await registry.getNextBondId(registryConfig.privateKey);
log('bondId:', bondId);
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
await registry.createBond(
{ denom: DENOM, amount: BOND_AMOUNT },
registryConfig.privateKey,
fee
registryConfig.fee
);
for await (const name of authorityNames) {
await registry.reserveAuthority({ name }, registryConfig.privateKey, fee);
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
log('Reserved authority name:', name);
await registry.setAuthorityBond(
{ name, bondId },
registryConfig.privateKey,
fee
registryConfig.fee
);
log(`Bond ${bondId} set for authority ${name}`);
}
+10 -10
View File
@@ -2,20 +2,22 @@ import debug from 'debug';
import { DataSource } from 'typeorm';
import path from 'path';
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
import { Registry } from '@snowballtools/laconic-sdk';
import { Config } from '../src/config';
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
import { getConfig } from '../src/utils';
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
const log = debug('snowball:publish-deploy-records');
async function main() {
const { registryConfig, database, misc } = await getConfig();
async function main () {
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const registry = new Registry(
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
{ chainId: registryConfig.chainId }
registryConfig.chainId
);
const dataSource = new DataSource({
@@ -59,8 +61,6 @@ async function main() {
url
};
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
const result = await registry.setRecord(
{
privateKey: registryConfig.privateKey,
@@ -68,12 +68,12 @@ async function main() {
bondId: registryConfig.bondId
},
'',
fee
registryConfig.fee
);
// Remove deployment for project subdomain if deployment is for production environment
if (deployment.environment === Environment.Production) {
applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.baseDomain}`;
applicationDeploymentRecord.url = `https://${deployment.project.subDomain}`
await registry.setRecord(
{
@@ -82,12 +82,12 @@ async function main() {
bondId: registryConfig.bondId
},
'',
fee
registryConfig.fee
);
}
log('Application deployment record data:', applicationDeploymentRecord);
log(`Application deployment record published: ${result.id}`);
log(`Application deployment record published: ${result.data.id}`);
}
}
@@ -2,20 +2,22 @@ import debug from 'debug';
import { DataSource } from 'typeorm';
import path from 'path';
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
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-deployment-removal-records');
async function main () {
const { registryConfig, database } = await getConfig();
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
const registry = new Registry(
registryConfig.gqlEndpoint,
registryConfig.restEndpoint,
{ chainId: registryConfig.chainId }
registryConfig.chainId
);
const dataSource = new DataSource({
@@ -45,8 +47,6 @@ async function main () {
request: deployment.applicationDeploymentRemovalRequestId,
}
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
const result = await registry.setRecord(
{
privateKey: registryConfig.privateKey,
@@ -54,11 +54,11 @@ async function main () {
bondId: registryConfig.bondId
},
'',
fee
registryConfig.fee
);
log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
log(`Application deployment removal record published: ${result.id}`);
log(`Application deployment removal record published: ${result.data.id}`);
}
}
+6 -13
View File
@@ -1,11 +1,9 @@
# deployer
- Install dependencies
```bash
yarn
```
```bash
brew install jq # if you do not have jq installed already
```
@@ -14,13 +12,13 @@
- To deploy frontend app to `dashboard.staging.apps.snowballtools.com`
```bash
```
./deploy-frontend.staging.sh
```
- To deploy frontend app to `dashboard.apps.snowballtools.com`
```bash
```
./deploy-frontend.sh
```
@@ -29,7 +27,6 @@
## Notes
- Any config env can be updated in [records/application-deployment-request.yml](records/application-deployment-request.yml)
```yml
record:
...
@@ -38,7 +35,6 @@
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
...
```
- On changing `LACONIC_HOSTED_CONFIG_app_github_clientid`, the GitHub client ID and secret need to be changed in backend config too
## Troubleshoot
@@ -48,17 +44,14 @@
- If deployment fails due to low bond balance
- Check balances
```bash
# Account balance
yarn laconic registry account get
yarn laconic cns account get
# Bond balance
yarn laconic registry bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
```
- Command to refill bond
```bash
yarn laconic registry bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type alnt --quantity 10000000
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
```
+4 -5
View File
@@ -1,10 +1,9 @@
services:
registry:
cns:
restEndpoint: 'http://console.laconic.com:1317'
gqlEndpoint: 'http://console.laconic.com:9473/api'
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
bondId: 098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661
chainId: laconic_9000-1
gas:
fees:
gasPrice: 1
gas: 550000
fees: 200000aphoton
+3 -4
View File
@@ -1,10 +1,9 @@
services:
registry:
cns:
restEndpoint: http://console.laconic.com:1317
gqlEndpoint: http://console.laconic.com:9473/api
userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
chainId: laconic_9000-1
gas:
fees:
gasPrice: 1
gas: 1200000
fees: 200000aphoton
+18 -50
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# Repository URL
REPO_URL="https://github.com/snowball-tools/snowballtools-base"
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
# Get the latest commit hash from the repository
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
@@ -18,7 +18,7 @@ REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
# Get latest version from registry and increment application-record version
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found
@@ -26,20 +26,20 @@ if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; t
fi
# Generate application-deployment-request.yml
cat >./records/application-deployment-request.yml <<EOF
cat > ./records/application-deployment-request.yml <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: snowballtools-base-frontend@$PACKAGE_VERSION
application: lrn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
dns: dashboard
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
@@ -48,7 +48,7 @@ record:
EOF
# Generate application-record.yml with incremented version
cat >./records/application-record.yml <<EOF
cat > ./records/application-record.yml <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
@@ -64,64 +64,32 @@ echo "Files generated successfully."
RECORD_FILE=records/application-record.yml
# Publish ApplicationRecord
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to publish record"
exit $rc
fi
RECORD_ID=$(echo $publish_response | jq -r '.id')
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationRecord published"
echo $RECORD_ID
# Set name to record
REGISTRY_APP_LRN="lrn://snowballtools/applications/snowballtools-base-frontend"
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
sleep 2
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
exit $rc
fi
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
sleep 2
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set hash"
exit $rc
fi
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
sleep 2
# Set name if latest release
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set release"
exit $rc
fi
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_LRN
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query name"
exit $rc
fi
APP_RECORD=$(echo $query_response | jq '.[0]')
# Check if record found for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_LRN."
echo "No record found for $REGISTRY_APP_CRN."
exit 1
fi
RECORD_FILE=records/application-deployment-request.yml
sleep 2
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query deployment request"
exit $rc
fi
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID
+20 -52
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# Repository URL
REPO_URL="https://github.com/snowball-tools/snowballtools-base"
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
# Get the latest commit hash from the repository
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
@@ -18,7 +18,7 @@ REGISTRY_BOND_ID="098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb466
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
# Get latest version from registry and increment application-record version
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "staging-snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "staging-snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found
@@ -26,26 +26,26 @@ if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; t
fi
# Generate application-deployment-request.yml
cat >./staging-records/application-deployment-request.yml <<EOF
cat > ./staging-records/application-deployment-request.yml <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: staging-snowballtools-base-frontend@$PACKAGE_VERSION
application: lrn://staging-snowballtools/applications/staging-snowballtools-base-frontend@$PACKAGE_VERSION
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@$PACKAGE_VERSION
dns: dashboard.staging.apps.snowballtools.com
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
meta:
note: Added by Snowball @ $CURRENT_DATE_TIME
@@ -54,7 +54,7 @@ record:
EOF
# Generate application-record.yml with incremented version
cat >./staging-records/application-record.yml <<EOF
cat > ./staging-records/application-record.yml <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
@@ -70,64 +70,32 @@ echo "Files generated successfully."
RECORD_FILE=staging-records/application-record.yml
# Publish ApplicationRecord
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to publish record"
exit $rc
fi
RECORD_ID=$(echo $publish_response | jq -r '.id')
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationRecord published"
echo $RECORD_ID
# Set name to record
REGISTRY_APP_LRN="lrn://staging-snowballtools/applications/staging-snowballtools-base-frontend"
REGISTRY_APP_CRN="crn://staging-snowballtools/applications/staging-snowballtools-base-frontend"
sleep 2
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
exit $rc
fi
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
sleep 2
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set hash"
exit $rc
fi
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
sleep 2
# Set name if latest release
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set release"
exit $rc
fi
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record found for REGISTRY_APP_LRN
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query name"
exit $rc
fi
APP_RECORD=$(echo $query_response | jq '.[0]')
# Check if record found for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_LRN."
echo "No record found for $REGISTRY_APP_CRN."
exit 1
fi
RECORD_FILE=staging-records/application-deployment-request.yml
sleep 2
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query deployment request"
exit $rc
fi
DEPLOYMENT_REQUEST_ID=$(echo $deployment_response | jq -r '.id')
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID
@@ -1,6 +1,6 @@
record:
type: ApplicationDeploymentRequest
version: "1.0.0"
version: '1.0.0'
name: snowballtools-base-frontend@0.1.8
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
dns: dashboard
@@ -8,11 +8,11 @@ record:
env:
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
meta:
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
repository: "https://github.com/snowball-tools/snowballtools-base"
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
@@ -2,7 +2,7 @@ record:
type: ApplicationRecord
version: 0.0.2
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
repository: ["https://github.com/snowball-tools/snowballtools-base"]
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp
name: snowballtools-base-frontend
app_version: 0.1.8
@@ -7,18 +7,18 @@ record:
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
meta:
note: Added by Snowball @ Mon Jun 24 23:51:48 UTC 2024
repository: "https://github.com/snowball-tools/snowballtools-base"
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
note: Added by Snowball @ Tuesday 21 May 2024 06:17:23 AM UTC
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
@@ -1,8 +1,8 @@
record:
type: ApplicationRecord
version: 0.0.1
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
repository: ["https://github.com/snowball-tools/snowballtools-base"]
version: 0.0.2
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp
name: staging-snowballtools-base-frontend
app_version: 0.0.0
+1 -1
View File
@@ -2,7 +2,7 @@
Check if the live web app deployer is in a working state
- Web app repo used: <https://github.com/snowball-tools/test-progressive-web-app> (main branch)
- Web app repo used: https://github.com/snowball-tools-platform/test-progressive-web-app (main branch)
- Config used: [../config.yml](../config.yml)
- The script [test-webapp-deployment-undeployment.sh](./test-webapp-deployment-undeployment.sh) performs the following:
- Create / update [`ApplicationRecord`](./records/application-record.yml) and [`ApplicationDeploymentRequest`](./records/application-deployment-request.yml) records with latest meta data from the repo
@@ -1,6 +1,6 @@
record:
type: ApplicationDeploymentRequest
version: "1.0.0"
version: '1.0.0'
name: deployment-test-app@0.1.24
application: crn://snowballtools/applications/deployment-test-app@0.1.24
dns: deployment-ci-test
@@ -11,5 +11,5 @@ record:
CERC_WEBAPP_DEBUG: 0
meta:
note: Deployment test @ Thu 11 Apr 2024 07:29:19 AM UTC
repository: "https://github.com/snowball-tools/test-progressive-web-app"
repository: "https://github.com/snowball-tools-platform/test-progressive-web-app"
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
@@ -2,7 +2,7 @@ record:
type: ApplicationRecord
version: 0.0.1
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
repository: ["https://github.com/snowball-tools/test-progressive-web-app"]
repository: ["https://github.com/snowball-tools-platform/test-progressive-web-app"]
app_type: webapp
name: deployment-test-app
app_version: 0.1.24
@@ -1,7 +1,7 @@
#!/bin/bash
# Repository URL
REPO_URL="https://github.com/snowball-tools/test-progressive-web-app"
REPO_URL="https://github.com/snowball-tools-platform/test-progressive-web-app"
# Get the latest commit hash from the repository
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
@@ -22,7 +22,7 @@ REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be
APP_NAME=deployment-test-app
# Get latest version from registry and increment application-record version
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found
@@ -32,7 +32,7 @@ fi
# Generate application-record.yml with incremented version
RECORD_FILE=packages/deployer/test/records/application-record.yml
cat >$RECORD_FILE <<EOF
cat > $RECORD_FILE <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
@@ -46,12 +46,12 @@ EOF
# Generate application-deployment-request.yml
REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-request.yml
cat >$REQUEST_RECORD_FILE <<EOF
cat > $REQUEST_RECORD_FILE <<EOF
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: $APP_NAME@$PACKAGE_VERSION
application: lrn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
application: crn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
dns: deployment-ci-test
config:
env:
@@ -67,31 +67,31 @@ EOF
echo "Record files generated successfully."
# Publish ApplicationRecord
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE | jq -r '.id')
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
echo "ApplicationRecord published"
echo $RECORD_ID
# Set name to record
REGISTRY_APP_LRN="lrn://snowballtools/applications/$APP_NAME"
REGISTRY_APP_CRN="crn://snowballtools/applications/$APP_NAME"
sleep 2
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
sleep 2
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
sleep 2
# Set name if latest release
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
# Check if record exists for REGISTRY_APP_LRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN" | jq '.[0]')
# Check if record exists for REGISTRY_APP_CRN
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_LRN."
echo "No record found for $REGISTRY_APP_CRN."
exit 1
fi
sleep 2
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRequest published"
echo $DEPLOYMENT_REQUEST_ID
@@ -102,7 +102,7 @@ MAX_RETRIES=20
# Check that a ApplicationDeploymentRecord is published
retry_count=0
while true; do
deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
len_deployment_records=$(echo $deployment_records_response | jq 'length')
# Check if number of records returned is 0
@@ -115,7 +115,7 @@ while true; do
else
echo "ApplicationDeploymentRecord not found, retrying in $RETRY_INTERVAL sec..."
sleep $RETRY_INTERVAL
retry_count=$((retry_count + 1))
retry_count=$((retry_count+1))
fi
else
echo "ApplicationDeploymentRecord found"
@@ -140,7 +140,7 @@ fetched_url=$(echo $deployment_records_response | jq -r '.[0].attributes.url')
retry_count=0
max_retries=10
retry_interval=10
retry_interval=5
while true; do
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
if [ "$url_response" = "200" ]; then
@@ -154,7 +154,7 @@ while true; do
else
echo "Deployment URL $fetched_url is not active, received code $url_response, retrying in $retry_interval sec..."
sleep $retry_interval
retry_count=$((retry_count + 1))
retry_count=$((retry_count+1))
fi
fi
done
@@ -162,7 +162,7 @@ done
# Generate application-deployment-removal-request.yml
REMOVAL_REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-removal-request.yml
cat >$REMOVAL_REQUEST_RECORD_FILE <<EOF
cat > $REMOVAL_REQUEST_RECORD_FILE <<EOF
record:
deployment: $DEPLOYMENT_RECORD_ID
type: ApplicationDeploymentRemovalRequest
@@ -170,14 +170,14 @@ record:
EOF
sleep 2
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRemovalRequest published"
echo $REMOVAL_REQUEST_ID
# Check that an ApplicationDeploymentRemovalRecord is published
retry_count=0
while true; do
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
len_removal_records=$(echo $removal_records_response | jq 'length')
# Check if number of records returned is 0
@@ -190,7 +190,7 @@ while true; do
else
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
sleep $RETRY_INTERVAL
retry_count=$((retry_count + 1))
retry_count=$((retry_count+1))
fi
else
echo "ApplicationDeploymentRemovalRecord found"
@@ -217,7 +217,7 @@ while true; do
else
echo "Deployment URL $fetched_url is still active, received code $url_response, retrying in $retry_interval sec..."
sleep $retry_interval
retry_count=$((retry_count + 1))
retry_count=$((retry_count+1))
fi
fi
done
+1 -1
View File
@@ -14,4 +14,4 @@ VITE_BUGSNAG_API_KEY=
VITE_PASSKEY_WALLET_RPID=
VITE_TURNKEY_API_BASE_URL=
VITE_TURNKEY_ORGANIZATION_ID=
VITE_TURNKEY_ORGANIZATION_ID=
-2
View File
@@ -9,7 +9,6 @@ import { join, dirname } from 'path';
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json')));
}
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
@@ -27,7 +26,6 @@ const config: StorybookConfig = {
docs: {
autodocs: 'tag',
},
staticDirs: ['../public'],
};
export default config;
+23 -56
View File
@@ -1,63 +1,30 @@
# frontend
# React + TypeScript + Vite
This is a [vite](https://vitejs.dev/) [react](https://reactjs.org/) [nextjs](https://nextjs.org/) project in a [yarn workspace](https://yarnpkg.com/features/workspaces).
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
## Getting Started
Currently, two official plugins are available:
### Install dependencies
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
In the root of the project, run:
## Expanding the ESLint configuration
```zsh
yarn
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
};
```
### Build backend
```zsh
yarn build --ignore frontend
```
### Environment variables
#### Local
Copy the `.env.example` file to `.env`:
```zsh
cp .env.example .env
```
#### Staging environment variables
Change in [deployer/deploy-frontend.staging.sh](/packages/deployer/deploy-frontend.staging.sh)
#### Production environment variables
Change in [deployer/deploy-frontend.sh](/packages/deployer/deploy-frontend.sh)
### Run development server
```zsh
yarn dev
```
## Deployment
From the root of the project,
### Staging
```zsh
cd packages/deployer && ./deploy-frontend.staging.sh
```
### Production
```zsh
cd packages/deployer && ./deploy-frontend.sh
```
### Deployment status
Check the status of the deployment [here](https://webapp-deployer.apps.snowballtools.com)
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
+1 -3
View File
@@ -16,11 +16,8 @@
"@bugsnag/browser-performance": "^2.4.1",
"@bugsnag/js": "^7.22.7",
"@bugsnag/plugin-react": "^7.22.7",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource-variable/jetbrains-mono": "^5.0.19",
"@fontsource/inter": "^5.0.16",
"@mui/material": "^6.1.3",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
@@ -65,6 +62,7 @@
"react-dom": "^18.2.0",
"react-dropdown": "^1.11.0",
"react-hook-form": "^7.49.0",
"react-hot-toast": "^2.4.1",
"react-oauth-popup": "^1.0.5",
"react-router-dom": "^6.20.1",
"react-timer-hook": "^3.0.7",
+2 -3
View File
@@ -1,4 +1,3 @@
import { useEffect } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Projects from './pages/org-slug';
@@ -11,8 +10,8 @@ import ProjectSearchLayout from './layouts/ProjectSearch';
import Index from './pages';
import AuthPage from './pages/AuthPage';
import { DashboardLayout } from './pages/org-slug/layout';
import { useEffect } from 'react';
import Web3Provider from 'context/Web3Provider';
import { baseUrl } from 'utils/constants';
const router = createBrowserRouter([
{
@@ -60,7 +59,7 @@ function App() {
// Hacky way of checking session
// TODO: Handle redirect backs
useEffect(() => {
fetch(`${baseUrl}/auth/session`, {
fetch(`${import.meta.env.VITE_SERVER_URL}/auth/session`, {
credentials: 'include',
}).then((res) => {
if (res.status !== 200) {
+2 -7
View File
@@ -1,21 +1,16 @@
import {
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO,
VITE_GITHUB_PWA_TEMPLATE_REPO,
} from 'utils/constants';
export default [
{
id: '1',
name: 'Progressive Web App (PWA)',
icon: 'pwa',
repoFullName: `${VITE_GITHUB_PWA_TEMPLATE_REPO}`,
repoFullName: `${import.meta.env.VITE_GITHUB_PWA_TEMPLATE_REPO}`,
isComingSoon: false,
},
{
id: '2',
name: 'Image Upload PWA',
icon: 'pwa',
repoFullName: `${VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
repoFullName: `${import.meta.env.VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
isComingSoon: false,
},
{
@@ -6,7 +6,7 @@ import { Input, InputProps } from './shared/Input';
const SearchBar: React.ForwardRefRenderFunction<
HTMLInputElement,
InputProps & RefAttributes<HTMLInputElement>
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
> = ({ value, onChange, placeholder = 'Search', ...props }) => {
return (
<div className="relative flex w-full">
<Input
@@ -18,7 +18,6 @@ const SearchBar: React.ForwardRefRenderFunction<
appearance="borderless"
className="w-full lg:w-[459px]"
{...props}
ref={ref}
/>
</div>
);
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { useCombobox } from 'downshift';
import { Project } from 'gql-client';
import { useDebounceValue } from 'usehooks-ts';
import { useDebounce } from 'usehooks-ts';
import SearchBar from 'components/SearchBar';
import { useGQLClient } from 'context/GQLClientContext';
@@ -42,7 +42,7 @@ export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
},
});
const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300);
const debouncedInputValue = useDebounce<string>(inputValue, 300);
const fetchProjects = useCallback(
async (inputValue: string) => {
@@ -62,7 +62,7 @@ export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
<div className="relative w-full lg:w-fit">
<SearchBar {...getInputProps()} />
<div
{...getMenuProps({}, { suppressRefError: true })}
{...getMenuProps()}
className={cn(
'flex flex-col shadow-dropdown rounded-xl bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
{ hidden: !inputValue || !isOpen },
@@ -5,7 +5,7 @@ import { CrossIcon, SearchIcon } from 'components/shared/CustomIcon';
import { Input } from 'components/shared/Input';
import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client';
import { useDebounceValue } from 'usehooks-ts';
import { useDebounce } from 'usehooks-ts';
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
import { useNavigate } from 'react-router-dom';
@@ -27,30 +27,25 @@ export const ProjectSearchBarDialog = ({
const client = useGQLClient();
const navigate = useNavigate();
const {
getInputProps,
getItemProps,
getMenuProps,
inputValue,
setInputValue,
} = useCombobox({
items,
itemToString(item) {
return item ? item.name : '';
},
selectedItem,
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
if (newSelectedItem) {
setSelectedItem(newSelectedItem);
onClickItem?.(newSelectedItem);
navigate(
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`,
);
}
},
});
const { getInputProps, getItemProps, inputValue, setInputValue } =
useCombobox({
items,
itemToString(item) {
return item ? item.name : '';
},
selectedItem,
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
if (newSelectedItem) {
setSelectedItem(newSelectedItem);
onClickItem?.(newSelectedItem);
navigate(
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`,
);
}
},
});
const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300);
const debouncedInputValue = useDebounce<string>(inputValue, 300);
const fetchProjects = useCallback(
async (inputValue: string) => {
@@ -80,7 +75,7 @@ export const ProjectSearchBarDialog = ({
<div className="h-full flex flex-col fixed top-0 inset-0">
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
<Input
{...getInputProps({}, { suppressRefError: true })}
{...getInputProps()}
leftIcon={<SearchIcon />}
placeholder="Search"
appearance="borderless"
@@ -91,33 +86,23 @@ export const ProjectSearchBarDialog = ({
</Button>
</div>
{/* Content */}
<div
className="flex flex-col gap-1 px-2 py-2"
{...getMenuProps(
{},
{
suppressRefError: true,
},
)}
>
{items.length > 0 ? (
<>
<div className="px-2 py-2">
<p className="text-elements-mid-em text-xs font-medium">
Suggestions
</p>
</div>
{items.map((item, index) => (
<ProjectSearchBarItem
key={item.id}
item={item}
{...getItemProps({ item, index })}
/>
))}
</>
) : (
inputValue && <ProjectSearchBarEmpty />
)}
<div className="flex flex-col gap-1 px-2 py-2">
{items.length > 0
? items.map((item, index) => (
<>
<div className="px-2 py-2">
<p className="text-elements-mid-em text-xs font-medium">
Suggestions
</p>
</div>
<ProjectSearchBarItem
key={item.id}
item={item}
{...getItemProps({ item, index })}
/>
</>
))
: inputValue && <ProjectSearchBarEmpty />}
</div>
</div>
</Dialog.Content>
@@ -1,236 +0,0 @@
import { useCallback, useState } from 'react';
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { useMediaQuery } from 'usehooks-ts';
import { AuctionData } from 'gql-client';
import {
ArrowRightCircleFilledIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
import { Heading } from '../../shared/Heading';
import { Button } from '../../shared/Button';
import { Select, SelectOption } from 'components/shared/Select';
import { Input } from 'components/shared/Input';
import { useToast } from 'components/shared/Toast';
import { useGQLClient } from '../../../context/GQLClientContext';
type ConfigureFormValues = {
option: string;
lrn?: string;
numProviders?: number;
maxPrice?: string;
};
const Configure = () => {
const [searchParams] = useSearchParams();
const templateId = searchParams.get('templateId');
const location = useLocation();
const { templateOwner, templateRepo, owner, name, isPrivate, orgSlug, repository } = location.state || {};
const navigate = useNavigate();
const { toast, dismiss } = useToast();
const client = useGQLClient();
const [isLoading, setIsLoading] = useState(false);
const { handleSubmit, control, watch } = useForm<ConfigureFormValues>({
defaultValues: { option: 'LRN' },
});
const selectedOption = watch('option');
const isTabletView = useMediaQuery('(min-width: 720px)'); // md:
const buttonSize = isTabletView ? { size: 'lg' as const } : {};
const onSubmit: SubmitHandler<ConfigureFormValues> = useCallback(
async (data) => {
setIsLoading(true);
try {
let lrn: string | undefined;
let auctionData: AuctionData | undefined;
if (data.option === 'LRN') {
lrn = data.lrn;
} else if (data.option === 'Auction') {
auctionData = {
numProviders: Number(data.numProviders!),
maxPrice: (data.maxPrice!).toString(),
};
}
if (templateId) {
// Template-based project creation
const projectData: any = {
templateOwner,
templateRepo,
owner,
name,
isPrivate,
};
const { addProjectFromTemplate } = await client.addProjectFromTemplate(
orgSlug,
projectData,
lrn,
auctionData
);
data.option === 'Auction'
? navigate(
`/${orgSlug}/projects/create/success/${addProjectFromTemplate.id}`,
{ state: { isAuction: true } }
)
: navigate(
`/${orgSlug}/projects/create/template/deploy?projectId=${addProjectFromTemplate.id}&templateId=${templateId}`
);
} else {
const { addProject } = await client.addProject(
orgSlug,
{
name: repository.fullName,
prodBranch: repository.defaultBranch,
repository: repository.fullName,
template: 'webapp',
},
lrn,
auctionData
);
data.option === 'Auction'
? navigate(
`/${orgSlug}/projects/create/success/${addProject.id}`,
{ state: { isAuction: true } }
)
: navigate(
`/${orgSlug}/projects/create/deploy?projectId=${addProject.id}`
);
}
} catch (error) {
console.error('Error creating project:', error);
toast({
id: 'error-creating-project',
title: 'Error creating project',
variant: 'error',
onDismiss: dismiss,
});
} finally {
setIsLoading(false);
}
},
[client, isPrivate, templateId, navigate, dismiss, toast]
);
return (
<div className="space-y-7">
<div className="flex justify-between">
<div className="space-y-1.5">
<Heading as="h4" className="md:text-lg font-medium">
Configure deployment
</Heading>
<Heading as="h5" className="text-sm font-sans text-elements-low-em">
The app can be deployed by setting the deployer LRN for a single
deployment or by creating a deployer auction for multiple
deployments
</Heading>
</div>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col gap-4 lg:gap-7 w-full">
<div className="flex flex-col justify-start gap-3">
<Controller
name="option"
control={control}
render={({ field: { value, onChange } }) => (
<Select
label="Configuration Options"
value={
{
value: value || 'LRN',
label: value === 'Auction' ? 'Create Auction' : 'Deployer LRN',
} as SelectOption
}
onChange={(value) => onChange((value as SelectOption).value)}
options={[
{ value: 'LRN', label: 'Deployer LRN' },
{ value: 'Auction', label: 'Create Auction' },
]}
/>
)}
/>
</div>
{selectedOption === 'LRN' && (
<div className="flex flex-col justify-start gap-3">
<Heading as="h5" className="text-sm font-sans text-elements-low-em">
The app will be deployed by the configured deployer
</Heading>
<span className="text-sm text-elements-high-em">
Enter LRN for deployer
</span>
<Controller
name="lrn"
control={control}
render={({ field: { value, onChange } }) => (
<Input value={value} onChange={onChange} />
)}
/>
</div>
)}
{selectedOption === 'Auction' && (
<>
<div className="flex flex-col justify-start gap-3">
<Heading as="h5" className="text-sm font-sans text-elements-low-em">
Set the number of deployers and maximum price for each deployment
</Heading>
<span className="text-sm text-elements-high-em">
Number of Deployers
</span>
<Controller
name="numProviders"
control={control}
render={({ field: { value, onChange } }) => (
<Input type="number" value={value} onChange={onChange} />
)}
/>
</div>
<div className="flex flex-col justify-start gap-3">
<span className="text-sm text-elements-high-em">
Maximum Price (alnt)
</span>
<Controller
name="maxPrice"
control={control}
render={({ field: { value, onChange } }) => (
<Input type="number" value={value} onChange={onChange} />
)}
/>
</div>
</>
)}
<div>
<Button
{...buttonSize}
type="submit"
disabled={isLoading}
rightIcon={
isLoading ? (
<LoadingIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
)
}
>
{isLoading ? 'Deploying repo' : 'Deploy repo'}
</Button>
</div>
</div>
</form>
</div>
);
};
export default Configure;
@@ -14,10 +14,11 @@ import { useToast } from '../../shared/Toast';
import { IconWithFrame } from '../../shared/IconWithFrame';
import { Heading } from '../../shared/Heading';
import { MockConnectGitCard } from './MockConnectGitCard';
import { VITE_GITHUB_CLIENT_ID } from 'utils/constants';
const SCOPES = 'repo user';
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${VITE_GITHUB_CLIENT_ID}&scope=${encodeURIComponent(SCOPES)}`;
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
import.meta.env.VITE_GITHUB_CLIENT_ID
}&scope=${encodeURIComponent(SCOPES)}`;
interface ConnectAccountInterface {
onAuth: (token: string) => void;
@@ -38,19 +38,36 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
});
}
navigate(`configure`,
{
state: {
repository: {
owner: repository.owner?.login,
name: repository.name,
defaultBranch: repository.default_branch,
fullName: repository.full_name,
},
orgSlug,
},
try {
setIsLoading(true);
const { addProject } = await client.addProject(orgSlug, {
name: `${repository.owner?.login}-${repository.name}`,
prodBranch: repository.default_branch as string,
repository: repository.full_name,
// TODO: Compute template from repo
template: 'webapp',
});
if (addProject) {
navigate(`import?projectId=${addProject.id}`);
} else {
toast({
id: 'failed-to-create-project',
title: 'Failed to create project',
variant: 'error',
onDismiss: dismiss,
});
}
);
} catch (error) {
console.error((error as Error).message);
toast({
id: 'failed-to-create-project',
title: 'Failed to create project',
variant: 'error',
onDismiss: dismiss,
});
} finally {
setIsLoading(false);
}
}, [client, repository, orgSlug, setIsLoading, navigate, toast]);
return (
@@ -1,8 +1,13 @@
import { CopyBlock, atomOneLight } from 'react-code-blocks';
import { Link } from 'react-router-dom';
import { Modal } from 'components/shared/Modal';
import { Button } from 'components/shared/Button';
import {
Button,
Dialog,
DialogHeader,
DialogBody,
DialogFooter,
} from '@snowballtools/material-tailwind-react-fork';
interface AssignDomainProps {
open: boolean;
@@ -11,31 +16,36 @@ interface AssignDomainProps {
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
return (
<Modal open={open} onOpenChange={handleOpen}>
<Modal.Content>
<Modal.Header>Assign Domain</Modal.Header>
<Modal.Body>
In order to assign a domain to your production deployments, configure
it in the{' '}
{/* TODO: Fix selection of project settings tab on navigation to domains */}
<Link to="../settings/domains" className="text-light-blue-800 inline">
project settings{' '}
</Link>
(recommended). If you want to assign to this specific deployment,
however, you can do so using our command-line interface:
{/* https://github.com/rajinwonderland/react-code-blocks/issues/138 */}
<CopyBlock
text="snowball alias <deployment> <domain>"
language=""
showLineNumbers={false}
theme={atomOneLight}
/>
</Modal.Body>
<Modal.Footer className="flex justify-start">
<Button onClick={handleOpen}>Okay</Button>
</Modal.Footer>
</Modal.Content>
</Modal>
<Dialog open={open} handler={handleOpen}>
<DialogHeader>Assign Domain</DialogHeader>
<DialogBody>
In order to assign a domain to your production deployments, configure it
in the{' '}
{/* TODO: Fix selection of project settings tab on navigation to domains */}
<Link to="../settings/domains" className="text-light-blue-800 inline">
project settings{' '}
</Link>
(recommended). If you want to assign to this specific deployment,
however, you can do so using our command-line interface:
{/* https://github.com/rajinwonderland/react-code-blocks/issues/138 */}
<CopyBlock
text="snowball alias <deployment> <domain>"
language=""
showLineNumbers={false}
theme={atomOneLight}
/>
</DialogBody>
<DialogFooter className="flex justify-start">
<Button
className="rounded-3xl"
variant="gradient"
color="blue"
onClick={handleOpen}
>
<span>Okay</span>
</Button>
</DialogFooter>
</Dialog>
);
};
@@ -83,7 +83,7 @@ const DeploymentDetailsCard = ({
return (
<div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative">
<div className="flex-1 flex justify-between w-full md:max-w-[30%] lg:max-w-[33%]">
<div className="flex-1 flex justify-between w-full md:max-w-[25%] lg:max-w-[28%]">
<div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full">
{/* DEPLOYMENT URL */}
{deployment.url && (
@@ -96,12 +96,7 @@ const DeploymentDetailsCard = ({
</OverflownText>
</Heading>
)}
{deployment.deployerLrn && (
<span className="text-sm text-elements-low-em tracking-tight block mt-2">
Deployer LRN: {deployment.deployerLrn}
</span>
)}
<span className="text-sm text-elements-low-em tracking-tight block">
<span className="text-sm text-elements-low-em tracking-tight">
{deployment.environment === Environment.Production
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
: 'Preview'}
@@ -1,12 +1,5 @@
import { useState, ComponentPropsWithRef } from 'react';
import {
Menu,
MenuHandler,
MenuItem,
MenuList,
} from '@snowballtools/material-tailwind-react-fork';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { Deployment, Domain, Environment, Project } from 'gql-client';
import { Button } from 'components/shared/Button';
import {
@@ -18,11 +11,17 @@ import {
UndoIcon,
CrossCircleIcon,
} from 'components/shared/CustomIcon';
import {
Menu,
MenuHandler,
MenuItem,
MenuList,
} from '@snowballtools/material-tailwind-react-fork';
import { ComponentPropsWithRef } from 'react';
import AssignDomainDialog from './AssignDomainDialog';
import { useGQLClient } from 'context/GQLClientContext';
import { cn } from 'utils/classnames';
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
import { useToast } from 'components/shared/Toast';
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
deployment: Deployment;
@@ -42,7 +41,6 @@ export const DeploymentMenu = ({
...props
}: DeploymentMenuProps) => {
const client = useGQLClient();
const { toast, dismiss } = useToast();
const [changeToProduction, setChangeToProduction] = useState(false);
const [redeployToProduction, setRedeployToProduction] = useState(false);
@@ -53,19 +51,9 @@ export const DeploymentMenu = ({
const isUpdated = await client.updateDeploymentToProd(deployment.id);
if (isUpdated) {
await onUpdate();
toast({
id: 'deployment_changed_to_production',
title: 'Deployment changed to production',
variant: 'success',
onDismiss: dismiss,
});
toast.success('Deployment changed to production');
} else {
toast({
id: 'deployment_not_changed_to_production',
title: 'Error changing deployment to production',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Unable to change deployment to production');
}
};
@@ -73,19 +61,9 @@ export const DeploymentMenu = ({
const isRedeployed = await client.redeployToProd(deployment.id);
if (isRedeployed) {
await onUpdate();
toast({
id: 'redeployed_to_production',
title: 'Redeployed to production',
variant: 'success',
onDismiss: dismiss,
});
toast.success('Redeployed to production');
} else {
toast({
id: 'redeployed_to_production_failed',
title: 'Error redeploying to production',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Unable to redeploy to production');
}
};
@@ -96,19 +74,9 @@ export const DeploymentMenu = ({
);
if (isRollbacked) {
await onUpdate();
toast({
id: 'deployment_rolled_back',
title: 'Deployment rolled back',
variant: 'success',
onDismiss: dismiss,
});
toast.success('Deployment rolled back');
} else {
toast({
id: 'deployment_rollback_failed',
title: 'Error rolling back deployment',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Unable to rollback deployment');
}
};
@@ -116,19 +84,9 @@ export const DeploymentMenu = ({
const isDeleted = await client.deleteDeployment(deployment.id);
if (isDeleted) {
await onUpdate();
toast({
id: 'deployment_deleted',
title: 'Deployment deleted',
variant: 'success',
onDismiss: dismiss,
});
toast.success('Deleted deployment');
} else {
toast({
id: 'deployment_not_deleted',
title: 'Error deleting deployment',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Unable to delete deployment');
}
};
@@ -1,116 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { Auction, Project } from 'gql-client';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '@mui/material';
import { CheckRoundFilledIcon, LoadingIcon } from 'components/shared/CustomIcon';
import { useGQLClient } from 'context/GQLClientContext';
import { Button, Heading, Tag } from 'components/shared';
const WAIT_DURATION = 5000;
export const AuctionCard = ({ project }: { project: Project }) => {
const [auctionStatus, setAuctionStatus] = useState<string>('');
const [deployerLrns, setDeployerLrns] = useState<string[]>([]);
const [auctionDetails, setAuctionDetails] = useState<Auction | null>(null);
const [openDialog, setOpenDialog] = useState<boolean>(false);
const client = useGQLClient();
const getIconByAuctionStatus = (status: string) =>
status === 'completed' ? <CheckRoundFilledIcon /> : <LoadingIcon className="animate-spin" />;
const checkAuctionStatus = useCallback(async () => {
const result = await client.getAuctionData(project.auctionId);
setAuctionStatus(result.status);
setAuctionDetails(result);
setDeployerLrns(project.deployerLrns);
}, [client, project.auctionId, project.deployerLrns]);
useEffect(() => {
const fetchUpdatedProject = async () => {
if (auctionStatus === 'completed') {
// Wait for 5 secs since the project is not immediately updated with deployer LRNs
await new Promise((resolve) => setTimeout(resolve, WAIT_DURATION));
const updatedProject = await client.getProject(project.id);
setDeployerLrns(updatedProject.project!.deployerLrns || []);
}
};
if (auctionStatus !== 'completed') {
const intervalId = setInterval(checkAuctionStatus, WAIT_DURATION);
checkAuctionStatus();
return () => clearInterval(intervalId);
} else {
fetchUpdatedProject();
}
}, [auctionStatus, checkAuctionStatus, client]);
const renderAuctionStatus = useCallback(
() => (
<Tag
leftIcon={getIconByAuctionStatus(auctionStatus)}
size="xs"
type={auctionStatus === 'completed' ? 'positive' : 'emphasized'}
>
{auctionStatus.toUpperCase()}
</Tag>
),
[auctionStatus]
);
const handleOpenDialog = () => setOpenDialog(true);
const handleCloseDialog = () => setOpenDialog(false);
return (
<>
<div className="p-3 gap-2 rounded-xl border border-gray-200 transition-colors hover:bg-base-bg-alternate flex flex-col mt-8">
<div className="flex justify-between items-center">
<Heading className="text-lg leading-6 font-medium">Auction details</Heading>
<Button onClick={handleOpenDialog} variant="tertiary" size="sm">
View details
</Button>
</div>
<div className="flex justify-between items-center mt-1">
<span className="text-elements-high-em text-sm font-medium tracking-tight">Auction Status</span>
<div className="ml-2">{renderAuctionStatus()}</div>
</div>
<div className="flex justify-between items-center mt-2">
<span className="text-elements-high-em text-sm font-medium tracking-tight">Auction Id</span>
<span className="text-elements-mid-em text-sm text-right">
{project.auctionId}
</span>
</div>
{deployerLrns?.length > 0 && (
<div className="mt-3">
<span className="text-elements-high-em text-sm font-medium tracking-tight">Deployer LRNs</span>
{deployerLrns.map((lrn, index) => (
<p key={index} className="text-elements-mid-em text-sm">
{'\u2022'} {lrn}
</p>
))}
</div>
)}
</div>
<Dialog open={openDialog} onClose={handleCloseDialog} fullWidth maxWidth="md">
<DialogTitle>Auction Details</DialogTitle>
<DialogContent>
{auctionDetails && <pre>{JSON.stringify(auctionDetails, null, 2)}</pre>}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>Close</Button>
</DialogActions>
</Dialog>
</>
);
};
@@ -60,8 +60,8 @@ const DeleteProjectDialog = ({
<Modal.Body>
<Input
label={
"Deleting your project is irreversible. Enter your project's name " + '"' +
project.name + '"' +
"Deleting your project is irreversible. Enter your project's name " +
project.name +
' below to confirm you want to permanently delete it:'
}
id="input"
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { Domain, DomainStatus, Project } from 'gql-client';
import {
Chip,
Typography,
Menu,
MenuHandler,
@@ -14,15 +15,6 @@ import EditDomainDialog from './EditDomainDialog';
import { useGQLClient } from 'context/GQLClientContext';
import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog';
import { useToast } from 'components/shared/Toast';
import { Tag } from 'components/shared/Tag';
import {
CheckIcon,
CrossIcon,
GearIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
import { Heading } from 'components/shared/Heading';
import { Button } from 'components/shared/Button';
enum RefreshStatus {
IDLE,
@@ -87,29 +79,22 @@ const DomainCard = ({
<>
<div className="flex justify-between py-3">
<div className="flex justify-start gap-1">
<Heading as="h6" className="flex-col">
{domain.name}{' '}
<Tag
type={
domain.status === DomainStatus.Live ? 'positive' : 'negative'
}
leftIcon={
domain.status === DomainStatus.Live ? (
<CheckIcon />
) : (
<CrossIcon />
)
}
>
{domain.status}
</Tag>
</Heading>
<Typography variant="h6">
<i>^</i> {domain.name}
</Typography>
<Chip
className="w-fit capitalize"
value={domain.status}
color={domain.status === DomainStatus.Live ? 'green' : 'orange'}
variant="ghost"
icon={<i>^</i>}
/>
</div>
<div className="flex justify-start gap-1">
<i
id="refresh"
className="cursor-pointer"
className="cursor-pointer w-8 h-8"
onClick={() => {
SetRefreshStatus(RefreshStatus.CHECKING);
setTimeout(() => {
@@ -117,17 +102,11 @@ const DomainCard = ({
}, CHECK_FAIL_TIMEOUT);
}}
>
{refreshStatus === RefreshStatus.CHECKING ? (
<LoadingIcon className="animate-spin" />
) : (
'L'
)}
{refreshStatus === RefreshStatus.CHECKING ? 'L' : 'R'}
</i>
<Menu placement="bottom-end">
<MenuHandler>
<Button iconOnly>
<GearIcon />
</Button>
<button className="border-2 rounded-full w-8 h-8">...</button>
</MenuHandler>
<MenuList>
<MenuItem
@@ -164,13 +143,13 @@ const DomainCard = ({
{domain.status === DomainStatus.Pending && (
<Card className="bg-slate-100 p-4 text-sm">
{refreshStatus === RefreshStatus.IDLE ? (
<Heading>
<Typography variant="small">
^ Add these records to your domain and refresh to check
</Heading>
</Typography>
) : refreshStatus === RefreshStatus.CHECKING ? (
<Heading className="text-blue-500">
<Typography variant="small" className="text-blue-500">
^ Checking records for {domain.name}
</Heading>
</Typography>
) : (
<div className="flex gap-2 text-red-500 mb-2">
<div className="grow">
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo } from 'react';
import { Controller, useForm, SubmitHandler } from 'react-hook-form';
import toast from 'react-hot-toast';
import { Domain } from 'gql-client';
import {
@@ -8,11 +9,10 @@ import {
Option,
} from '@snowballtools/material-tailwind-react-fork';
import { useGQLClient } from 'context/GQLClientContext';
import { useGQLClient } from '../../../../context/GQLClientContext';
import { Modal } from 'components/shared/Modal';
import { Button } from 'components/shared/Button';
import { Input } from 'components/shared/Input';
import { useToast } from 'components/shared/Toast';
const DEFAULT_REDIRECT_OPTIONS = ['none'];
@@ -40,7 +40,6 @@ const EditDomainDialog = ({
onUpdate,
}: EditDomainDialogProp) => {
const client = useGQLClient();
const { toast, dismiss } = useToast();
const getRedirectUrl = (domain: Domain) => {
const redirectDomain = domain.redirectTo;
@@ -100,20 +99,10 @@ const EditDomainDialog = ({
if (updateDomain) {
await onUpdate();
toast({
id: 'domain_id_updated',
title: `Domain ${domain.name} has been updated`,
variant: 'success',
onDismiss: dismiss,
});
toast.success(`Domain ${domain.name} has been updated`);
} else {
reset();
toast({
id: 'domain_id_error_update',
title: `Error updating domain ${domain.name}`,
variant: 'error',
onDismiss: dismiss,
});
toast.error(`Error updating domain ${domain.name}`);
}
handleOpen();
@@ -3,8 +3,6 @@ import { useState } from 'react';
import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog';
import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast';
import { Input } from 'components/shared/Input';
import { CopyIcon, TrashIcon } from 'components/shared/CustomIcon';
interface WebhookCardProps {
webhookUrl: string;
@@ -16,12 +14,11 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
return (
<div className="flex justify-between w-full mb-3 gap-3">
<Input value={webhookUrl} disabled />
<div className="flex justify-between w-full mb-3">
{webhookUrl}
<div className="flex gap-3">
<Button
iconOnly
size="md"
size="sm"
onClick={() => {
navigator.clipboard.writeText(webhookUrl);
toast({
@@ -32,17 +29,16 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
});
}}
>
<CopyIcon />
Copy
</Button>
<Button
iconOnly
size="md"
size="sm"
variant="danger"
onClick={() => {
setDeleteDialogOpen(true);
}}
>
<TrashIcon />
X
</Button>
</div>
<DeleteWebhookDialog
@@ -4,6 +4,7 @@ import {
useMemo,
ComponentPropsWithoutRef,
} from 'react';
import { FieldValues, UseFormRegister } from 'react-hook-form';
import { WarningIcon } from 'components/shared/CustomIcon';
import { cloneIcon } from 'utils/cloneIcon';
@@ -11,7 +12,7 @@ import { cn } from 'utils/classnames';
import { InputTheme, inputTheme } from './Input.theme';
export interface InputProps
export interface InputProps<T extends FieldValues = FieldValues>
extends InputTheme,
Omit<ComponentPropsWithoutRef<'input'>, 'size'> {
label?: string;
@@ -19,6 +20,9 @@ export interface InputProps
leftIcon?: ReactNode;
rightIcon?: ReactNode;
helperText?: string;
// react-hook-form optional register
register?: ReturnType<UseFormRegister<T>>;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
@@ -30,6 +34,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
leftIcon,
rightIcon,
helperText,
register,
size,
state,
appearance,
@@ -102,11 +107,12 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
<div className={containerCls({ class: className })}>
{leftIcon && renderLeftIcon}
<input
{...(register ? register : {})}
className={cn(inputCls(), {
'pl-10': leftIcon,
})}
ref={ref}
{...props}
ref={ref}
/>
{rightIcon && renderRightIcon}
</div>
@@ -20,7 +20,6 @@ import { cn } from 'utils/classnames';
import { useMediaQuery } from 'usehooks-ts';
import { SIDEBAR_MENU } from './constants';
import { UserSelect } from 'components/shared/UserSelect';
import { baseUrl } from 'utils/constants';
interface SidebarProps {
mobileOpen?: boolean;
@@ -84,7 +83,7 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
}, [orgSlug]);
const handleLogOut = useCallback(async () => {
await fetch(`${baseUrl}/auth/logout`, {
await fetch(`${import.meta.env.VITE_SERVER_URL}/auth/logout`, {
method: 'POST',
credentials: 'include',
});
@@ -1,117 +0,0 @@
import { useEffect, useRef } from 'react';
import { Input } from '../Input';
export interface VerifyCodeInputProps {
code: string;
setCode: (code: string) => void;
submitCode: () => void;
loading: boolean;
}
export const VerifyCodeInput = ({
code,
setCode,
submitCode,
loading,
}: VerifyCodeInputProps) => {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const handlePaste = (
e: React.ClipboardEvent<HTMLInputElement>,
i: number,
) => {
e.preventDefault();
const pasteData = e.clipboardData.getData('text').replace(/\D/g, ''); // Only digits
if (pasteData.length > 0) {
let newCodeArray = code.split('');
for (let j = 0; j < pasteData.length && i + j < 6; j++) {
newCodeArray[i + j] = pasteData[j];
}
const newCode = newCodeArray.join('');
setCode(newCode);
const nextIndex = Math.min(i + pasteData.length, 5);
const nextInput = inputRefs.current[nextIndex];
if (nextInput) nextInput.focus();
if (!newCode.includes(' ')) {
submitCode();
}
}
};
const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement>,
i: number,
) => {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
if (e.key === 'Backspace') {
e.preventDefault();
const isEmpty = code[i] === ' ';
const newCode = !isEmpty
? code.slice(0, i) + ' ' + code.slice(i + 1, 6)
: code.slice(0, i - 1) + ' ' + code.slice(i, 6);
setCode(newCode.slice(0, 6));
if (i === 0 || !isEmpty) return;
const prev = inputRefs.current[i - 1];
if (prev) prev.focus();
return;
}
if (!e.key.match(/[0-9]/)) return;
e.preventDefault(); // Prevent the default event to avoid duplicate input
const newCode = code.slice(0, i) + e.key + code.slice(i + 1, 6);
setCode(newCode);
if (i === 5) {
submitCode();
return;
}
const next = inputRefs.current[i + 1];
if (next) next.focus();
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>, i: number) => {
const value = e.target.value.slice(-1);
if (!value.match(/[0-9]/)) return;
const newCode = code.slice(0, i) + value + code.slice(i + 1, 6);
setCode(newCode);
if (i < 5) {
const next = inputRefs.current[i + 1];
if (next) next.focus();
}
if (!newCode.includes(' ')) {
submitCode();
}
};
useEffect(() => {
if (inputRefs.current[0]) {
inputRefs.current[0].focus();
}
}, []);
return (
<div className="flex gap-2">
{code.split('').map((char, i) => (
<Input
key={i}
value={char === ' ' ? '' : char}
ref={(el) => (inputRefs.current[i] = el)}
onChange={(e) => handleChange(e, i)}
onPaste={(e) => handlePaste(e, i)}
onKeyDown={(e) => handleKeyDown(e, i)}
disabled={!!loading}
style={{ textAlign: 'center' }} // Add this line to center text
/>
))}
</div>
);
};
@@ -1 +0,0 @@
export { VerifyCodeInput } from './VerifyCodeInput';
@@ -1,27 +0,0 @@
export { Avatar } from './Avatar';
export { Badge } from './Badge';
export { Button } from './Button';
export { Calendar } from './Calendar';
export { Checkbox } from './Checkbox';
export { DatePicker } from './DatePicker';
export { DotBorder } from './DotBorder';
export { Heading } from './Heading';
export { IconWithFrame } from './IconWithFrame';
export { InlineNotification } from './InlineNotification';
export { Input } from './Input';
export { Modal } from './Modal';
export { OverflownText } from './OverflownText';
export { Radio } from './Radio';
export { SegmentedControls } from './SegmentedControls';
export { Select } from './Select';
export { Sidebar } from './Sidebar';
export { Step } from './Steps';
export { Switch } from './Switch';
export { Table } from './Table';
export { Tabs } from './Tabs';
export { Tag } from './Tag';
export { useToast } from './Toast';
export { Tooltip } from './Tooltip';
export { UserSelect } from './UserSelect';
export { VerifyCodeInput } from './VerifyCodeInput';
export { WavyBorder } from './WavyBorder';
@@ -1,11 +1,9 @@
import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { VITE_WALLET_CONNECT_ID } from 'utils/constants';
const queryClient = new QueryClient();
if (!VITE_WALLET_CONNECT_ID) {
if (!import.meta.env.VITE_WALLET_CONNECT_ID) {
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
}
+2 -5
View File
@@ -14,16 +14,13 @@ import { GQLClientProvider } from './context/GQLClientContext';
import { SERVER_GQL_PATH } from './constants';
import { Toaster } from 'components/shared/Toast';
import { LogErrorBoundary } from 'utils/log-error';
import { baseUrl } from 'utils/constants';
console.log(`v-0.0.9`);
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
assert(baseUrl, 'VITE_SERVER_URL is not set in env');
const gqlEndpoint = `${baseUrl}/${SERVER_GQL_PATH}`;
assert(import.meta.env.VITE_SERVER_URL, 'VITE_SERVER_URL is not set in env');
const gqlEndpoint = `${import.meta.env.VITE_SERVER_URL}/${SERVER_GQL_PATH}`;
const gqlClient = new GQLClient({ gqlEndpoint });
@@ -32,7 +32,7 @@ const ProjectSearch = () => {
return (
<section className="h-full flex flex-col">
{/* Header */}
<div className="sticky hidden lg:block top-0 border-b bg-base-bg border-border-separator/[0.06] hover:z-30">
<div className="sticky hidden lg:block top-0 border-b bg-base-bg border-border-separator/[0.06] z-30">
<div className="flex pr-6 pl-2 py-2 items-center">
<div className="flex-1">
<ProjectSearchBar
@@ -1,99 +0,0 @@
import React, { useState } from 'react';
import { Button } from 'components/shared/Button';
import {
ArrowRightCircleFilledIcon,
LoaderIcon,
} from 'components/shared/CustomIcon';
import { WavyBorder } from 'components/shared/WavyBorder';
import { VerifyCodeInput } from 'components/shared/VerifyCodeInput';
import { verifyAccessCode } from 'utils/accessCode';
type AccessMethod = 'accesscode' | 'passkey';
type Err = { type: AccessMethod; message: string };
type AccessCodeProps = {
onCorrectAccessCode: () => void;
};
export const AccessCode: React.FC<AccessCodeProps> = ({
onCorrectAccessCode,
}) => {
const [accessCode, setAccessCode] = useState(' ');
const [error, setError] = useState<Err | null>();
const [accessMethod, setAccessMethod] = useState<AccessMethod | false>(false);
async function validateAccessCode() {
setAccessMethod('accesscode');
try {
const isValidAccessCode = await verifyAccessCode(accessCode);
// add a pause for ux
await new Promise((resolve) => setTimeout(resolve, 250));
if (isValidAccessCode) {
localStorage.setItem('accessCode', accessCode);
onCorrectAccessCode();
} else {
setError({
type: 'accesscode',
message: 'Invalid access code',
});
}
} catch (err: any) {
setError({ type: 'accesscode', message: err.message });
}
}
const loading = accessMethod;
const isValidAccessCodeLength = accessCode.trim().length === 5;
return (
<div>
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-tight">
Access Code
</div>
</div>
<WavyBorder className="self-stretch" variant="stroke" />
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
<div className="self-stretch flex-col gap-8 flex">
<div className="flex-col justify-start items-start gap-2 inline-flex">
<VerifyCodeInput
loading={!!loading}
code={accessCode}
setCode={setAccessCode}
submitCode={validateAccessCode}
/>
</div>
<Button
rightIcon={
loading && loading === 'accesscode' ? (
<LoaderIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon height="16" />
)
}
onClick={validateAccessCode}
variant={'secondary'}
disabled={!accessCode || !isValidAccessCodeLength || !!loading}
>
Submit
</Button>
{error && error.type === 'accesscode' && (
<div className="flex flex-col gap-3">
<div className="justify-center items-center gap-2 inline-flex">
<div className="text-red-500 text-sm">
Error: {error.message}.{' '}
<a href="/signup" className="underline">
Try again?
</a>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
};
@@ -1,20 +0,0 @@
import React, { useState } from 'react';
import { AccessCode } from './AccessCode';
import { SignUp } from './SignUp';
type AccessSignUpProps = {
onDone: () => void;
};
export const AccessSignUp: React.FC<AccessSignUpProps> = ({ onDone }) => {
const [isValidAccessCode, setIsValidAccessCode] = useState<boolean>(
!!localStorage.getItem('accessCode'),
);
return isValidAccessCode ? (
<SignUp onDone={onDone} />
) : (
<AccessCode onCorrectAccessCode={() => setIsValidAccessCode(true)} />
);
};
@@ -47,9 +47,7 @@ export const Login = ({ onDone }: Props) => {
console.log('Handling google redirect back');
try {
await google.handleOAuthRedirectBack();
// @ts-ignore
wallet = await google.getEthersWallet();
// @ts-ignore
const result = await signInWithEthereum(1, 'login', wallet);
if (result.error) {
setError(result.error);
@@ -21,7 +21,6 @@ import {
turnkeySignin,
turnkeySignup,
} from 'utils/turnkey-frontend';
import { verifyAccessCode } from 'utils/accessCode';
type Provider = 'google' | 'github' | 'apple' | 'email';
@@ -46,9 +45,7 @@ export const SignUp = ({ onDone }: Props) => {
setProvider('google');
try {
await google.handleOAuthRedirectBack();
// @ts-ignore
wallet = await google.getEthersWallet();
// @ts-ignore
const result = await signInWithEthereum(1, 'signup', wallet);
if (result.error) {
setError({ type: 'provider', message: result.error });
@@ -112,29 +109,6 @@ export const SignUp = ({ onDone }: Props) => {
const loading = provider;
const emailValid = /.@./.test(email);
useEffect(() => {
const validateAccessCode = async () => {
const accessCode = localStorage.getItem('accessCode');
if (!accessCode) {
redirectToSignup();
return;
}
try {
await verifyAccessCode(accessCode);
} catch (err: any) {
redirectToSignup();
}
};
const redirectToSignup = () => {
localStorage.removeItem('accessCode');
window.location.href = '/signup';
};
validateAccessCode();
}, []);
return (
<div>
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
@@ -1,15 +1,14 @@
import React, { useEffect, useState } from 'react';
import { snowball } from 'utils/use-snowball';
import { Login } from './Login';
import { SignUp } from './SignUp';
import { useEffect, useState } from 'react';
import { Done } from './Done';
import { AccessSignUp } from './AccessSignUp';
type Screen = 'login' | 'signup' | 'success';
const DASHBOARD_URL = '/';
export const SnowballAuth: React.FC = () => {
export const SnowballAuth = () => {
const path = window.location.pathname;
const [screen, setScreen] = useState<Screen>(
path === '/login' ? 'login' : 'signup',
@@ -31,7 +30,7 @@ export const SnowballAuth: React.FC = () => {
if (screen === 'signup') {
return (
<AccessSignUp
<SignUp
onDone={() => {
setScreen('success');
}}
@@ -36,7 +36,6 @@ const deployment: Deployment = {
url: 'https://deploy1.example.com',
environment: Environment.Production,
isCurrent: true,
deployerLrn: 'lrn://deepstack-test4/deployers/webapp-deployer-api.test4.wireitin.com',
status: DeploymentStatus.Ready,
createdBy: {
id: 'user1',
@@ -1,19 +1,5 @@
import { Heading } from 'components/shared/Heading';
const Settings = () => {
return (
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
{/* Header */}
<div className="flex items-center">
<div className="grow">
<div className="flex gap-4 items-center">
<Heading as="h2" className="text-[24px]">
Settings
</Heading>
</div>
</div>
</div>
</section>
);
return <div className="p-5">Settings page</div>;
};
export default Settings;
@@ -1,11 +1,13 @@
import { useCallback, useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { ProjectCard } from 'components/projects/ProjectCard';
import { Heading, Badge, Button } from 'components/shared';
import { PlusIcon } from 'components/shared/CustomIcon';
import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client';
import { Button } from 'components/shared/Button';
import { PlusIcon } from 'components/shared/CustomIcon';
import { ProjectCard } from 'components/projects/ProjectCard';
import { Heading } from 'components/shared/Heading';
import { Badge } from 'components/shared/Badge';
import { useGQLClient } from 'context/GQLClientContext';
const Projects = () => {
const client = useGQLClient();
@@ -26,7 +28,6 @@ const Projects = () => {
return (
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
{/* Header */}
<div className="flex items-center">
<div className="grow">
<div className="flex gap-4 items-center">
@@ -31,11 +31,6 @@ const CreateWithTemplate = () => {
},
{
step: 2,
route: `/${orgSlug}/projects/create/template/configure`,
label: 'Configure',
},
{
step: 3,
route: `/${orgSlug}/projects/create/template/deploy`,
label: 'Deploy',
},
@@ -2,8 +2,7 @@ import NewProject from './index';
import CreateWithTemplate from './Template';
import { templateRoutes } from './template/routes';
import Id from './success/Id';
import Configure from 'components/projects/create/Configure';
import Deploy from 'components/projects/create/Deploy';
import Import from './Import';
export const createProjectRoutes = [
{
@@ -20,11 +19,7 @@ export const createProjectRoutes = [
element: <Id />,
},
{
path: 'configure',
element: <Configure />,
},
{
path: 'deploy',
element: <Deploy />,
path: 'import',
element: <Import />,
},
];
@@ -1,125 +1,108 @@
import { Link, useLocation, useParams } from 'react-router-dom';
import { Link, useParams } from 'react-router-dom';
import Lottie from 'lottie-react';
import { Badge } from 'components/shared/Badge';
import { Button } from 'components/shared/Button';
import {
ArrowLeftCircleFilledIcon,
LinkChainIcon,
QuestionMarkRoundFilledIcon,
} from 'components/shared/CustomIcon';
import { Heading } from 'components/shared/Heading';
import logoAnimation from 'components/../../public/lottie/logo.json';
import { useGQLClient } from 'context/GQLClientContext';
import { Project } from 'gql-client';
import { useEffect, useState } from 'react';
const Id = () => {
const { id, orgSlug } = useParams();
const client = useGQLClient();
const [project, setProject] = useState<Project | null>(null);
const location = useLocation();
const { isAuction } = location.state || {};
const handleSetupDomain = async () => {
if (id) {
// console.log('id', id);
// console.log('getting project for id', id);
const project = await client.getProject(id);
// console.log('project found:', project);
if (project && project.project) {
// console.log('project:', project.project);
setProject(project.project);
}
} else {
window.location.href = '/';
}
const handleSetupDomain = () => {
//TODO: Implement this
};
useEffect(() => {
handleSetupDomain();
});
return (
<>
{project ? (
<div className="flex flex-col gap-8 lg:gap-11 max-w-[522px] mx-auto py-6 lg:py-12">
{/* Icon */}
<div className="flex justify-center">
<Lottie animationData={logoAnimation} loop={false} size={40} />
</div>
<div className="flex flex-col gap-8 lg:gap-11 max-w-[522px] mx-auto py-6 lg:py-12">
{/* Icon */}
<div className="flex justify-center">
<Lottie animationData={logoAnimation} loop={false} size={40} />
</div>
{/* Heading */}
<div className="flex flex-col items-center gap-1.5">
<Heading as="h3" className="font-medium text-xl">
{isAuction? 'Auction created successfully.' : 'Project deployment created successfully.'}
</Heading>
</div>
{/* Heading */}
<div className="flex flex-col items-center gap-1.5">
<Heading as="h3" className="font-medium text-xl">
Project deployed successfully.
</Heading>
<p className="flex flex-col items-center lg:flex-row font-sans gap-0.5 lg:gap-2 text-sm text-elements-high-em">
Your project has been deployed at{' '}
<Button
className="no-underline text-elements-link"
// TODO: use dynamic value
href="https://www.iglootools.snowballtools.xyz"
as="a"
variant="link-emphasized"
external
leftIcon={<LinkChainIcon />}
>
{/* // TODO: use dynamic value */}
www.iglootools.snowballtools.xyz
</Button>
</p>
</div>
{/* Card */}
<div className="bg-base-bg-alternate rounded-xl shadow-inset w-full px-1 py-1">
{/* Trigger question */}
<div className="flex gap-2 justify-center items-center py-3">
<div className="h-5 w-5">
<QuestionMarkRoundFilledIcon size={18} />
{/* Card */}
<div className="bg-base-bg-alternate rounded-xl shadow-inset w-full px-1 py-1">
{/* Trigger question */}
<div className="flex gap-2 justify-center items-center py-3">
<div className="h-5 w-5">
<QuestionMarkRoundFilledIcon size={18} />
</div>
<Heading as="h5" className="font-sans font-medium text-sm">
{`Wondering what's next?`}
</Heading>
</div>
{/* CTA card */}
<div className="bg-surface-card rounded-xl shadow-card-sm px-4 py-4">
<div className="flex gap-2">
<Badge variant="secondary">1</Badge>
<div className="space-y-3">
<div className="flex flex-col gap-1">
<Heading as="h6" className="text-sm font-sans">
Add a custom domain
</Heading>
<p className="text-xs text-elements-low-em font-sans">
Make it easy for your visitors to remember your URL with a
custom domain.
</p>
</div>
<Heading as="h5" className="font-sans font-medium text-sm">
{`Wondering what's next?`}
</Heading>
</div>
{/* CTA card */}
<div className="bg-surface-card rounded-xl shadow-card-sm px-4 py-4">
<div className="flex gap-2">
<Badge variant="secondary">1</Badge>
<div className="space-y-3">
<div className="flex flex-col gap-1">
<Heading as="h6" className="text-sm font-sans">
Add a custom domain
</Heading>
<p className="text-xs text-elements-low-em font-sans">
Make it easy for your visitors to remember your URL with a
custom domain.
</p>
</div>
<Button
onClick={handleSetupDomain}
variant="tertiary"
size="sm"
>
Setup domain
</Button>
</div>
</div>
</div>
</div>
{/* CTA Buttons */}
<div className="flex flex-col lg:flex-row justify-center gap-3">
<div className="w-full lg:w-fit">
<Link to="/">
<Button
leftIcon={<ArrowLeftCircleFilledIcon />}
fullWidth
variant="tertiary"
>
Back to projects
</Button>
</Link>
</div>
<div className="w-full lg:w-fit">
<Link to={`/${orgSlug}/projects/${id}`}>
<Button fullWidth variant="primary">
View project
</Button>
</Link>
<Button onClick={handleSetupDomain} variant="tertiary" size="sm">
Setup domain
</Button>
</div>
</div>
</div>
) : (
<></>
)}
</>
</div>
{/* CTA Buttons */}
<div className="flex flex-col lg:flex-row justify-center gap-3">
<div className="w-full lg:w-fit">
<Link to="/">
<Button
leftIcon={<ArrowLeftCircleFilledIcon />}
fullWidth
variant="tertiary"
>
Back to projects
</Button>
</Link>
</div>
<div className="w-full lg:w-fit">
<Link to={`/${orgSlug}/projects/${id}`}>
<Button fullWidth variant="primary">
View project
</Button>
</Link>
</div>
</div>
</div>
);
};
@@ -1,7 +0,0 @@
import ConfigureComponent from '../../../../../components/projects/create/Configure';
const Configure = () => {
return <ConfigureComponent />;
};
export default Configure;
@@ -6,14 +6,12 @@ import { useMediaQuery } from 'usehooks-ts';
import { RequestError } from 'octokit';
import { useOctokit } from '../../../../../context/OctokitContext';
import { useGQLClient } from '../../../../../context/GQLClientContext';
import { Template } from '../../../../../types/types';
import { Heading } from 'components/shared/Heading';
import { Input } from 'components/shared/Input';
import { Select, SelectOption } from 'components/shared/Select';
import {
ArrowRightCircleFilledIcon,
LoadingIcon,
} from 'components/shared/CustomIcon';
import { ArrowRightCircleFilledIcon } from 'components/shared/CustomIcon';
import { Checkbox } from 'components/shared/Checkbox';
import { Button } from 'components/shared/Button';
import { useToast } from 'components/shared/Toast';
@@ -30,6 +28,7 @@ type SubmitRepoValues = {
const CreateRepo = () => {
const { octokit, isAuth } = useOctokit();
const { template } = useOutletContext<{ template: Template }>();
const client = useGQLClient();
const { orgSlug } = useParams();
const { toast, dismiss } = useToast();
@@ -51,21 +50,36 @@ const CreateRepo = () => {
assert(template.repoFullName, 'Template URL not provided');
const [owner, repo] = template.repoFullName.split('/');
setIsLoading(true);
// TODO: Handle this functionality in backend
const gitRepo = await octokit?.rest.repos.createUsingTemplate({
template_owner: owner,
template_repo: repo,
owner: data.account,
name: data.repoName,
include_all_branches: false,
private: data.isPrivate,
});
navigate(
`configure?templateId=${template.id}`,
{
state: {
templateOwner: owner,
templateRepo: repo,
owner: data.account,
name: data.repoName,
isPrivate: false,
orgSlug
},
}
);
if (!gitRepo) {
return;
}
// Refetch to always get correct default branch
const templateRepo = await octokit.rest.repos.get({
owner: template.repoFullName.split('/')[0],
repo: template.repoFullName.split('/')[1],
});
const prodBranch = templateRepo.data.default_branch ?? 'main';
const { addProject } = await client.addProject(orgSlug!, {
name: `${gitRepo.data.owner!.login}-${gitRepo.data.name}`,
prodBranch,
repository: gitRepo.data.full_name,
// TODO: Set selected template
template: 'webapp',
});
navigate(`deploy?projectId=${addProject.id}&templateId=${template.id}`);
} catch (err) {
setIsLoading(false);
@@ -192,15 +206,9 @@ const CreateRepo = () => {
{...buttonSize}
type="submit"
disabled={!Boolean(template.repoFullName) || isLoading}
rightIcon={
isLoading ? (
<LoadingIcon className="animate-spin" />
) : (
<ArrowRightCircleFilledIcon />
)
}
rightIcon={<ArrowRightCircleFilledIcon />}
>
Next
Deploy
</Button>
</div>
</div>
@@ -1,5 +1,4 @@
import CreateRepo from './index';
import Configure from './Configure';
import Deploy from './Deploy';
export const templateRoutes = [
@@ -7,10 +6,6 @@ export const templateRoutes = [
index: true,
element: <CreateRepo />,
},
{
path: 'configure',
element: <Configure />,
},
{
path: 'deploy',
element: <Deploy />,
@@ -1,11 +1,14 @@
import { useEffect, useState } from 'react';
import { Domain, DomainStatus } from 'gql-client';
import { Link, useNavigate, useOutletContext } from 'react-router-dom';
import { RequestError } from 'octokit';
import { useOctokit } from 'context/OctokitContext';
import { useOctokit } from '../../../../context/OctokitContext';
import { GitCommitWithBranch, OutletContextType } from '../../../../types';
import { useGQLClient } from 'context/GQLClientContext';
import { Button, Heading, Avatar, Tag } from 'components/shared';
import { useGQLClient } from '../../../../context/GQLClientContext';
import { Button } from 'components/shared/Button';
import { Heading } from 'components/shared/Heading';
import { Avatar } from 'components/shared/Avatar';
import { getInitials } from 'utils/geInitials';
import {
BranchStrokeIcon,
@@ -15,13 +18,12 @@ import {
GithubStrokeIcon,
GlobeIcon,
LinkIcon,
CalendarDaysIcon,
} from 'components/shared/CustomIcon';
import { Tag } from 'components/shared/Tag';
import { Activity } from 'components/projects/project/overview/Activity';
import { OverviewInfo } from 'components/projects/project/overview/OverviewInfo';
import { CalendarDaysIcon } from 'components/shared/CustomIcon/CalendarDaysIcon';
import { relativeTimeMs } from 'utils/time';
import { Domain, DomainStatus } from 'gql-client';
import { AuctionCard } from 'components/projects/project/overview/Activity/AuctionCard';
const COMMITS_PER_PAGE = 4;
@@ -129,15 +131,9 @@ const OverviewTabPanel = () => {
<Heading className="text-lg leading-6 font-medium truncate">
{project.name}
</Heading>
{project.baseDomains && project.baseDomains.length > 0 && project.baseDomains.map((baseDomain, index) => (
<a
key={index}
href={`https://${project.name}.${baseDomain}`}
className="text-sm text-elements-low-em tracking-tight truncate"
>
{baseDomain}
</a>
))}
<p className="text-sm text-elements-low-em tracking-tight truncate">
{project.subDomain}
</p>
</div>
</div>
<OverviewInfo label="Domain" icon={<GlobeIcon />}>
@@ -209,7 +205,6 @@ const OverviewTabPanel = () => {
No current deployment found.
</p>
)}
{project.auctionId && <AuctionCard project={project}/>}
</div>
<Activity activities={activities} isLoading={fetchingActivities} />
</div>
@@ -1,12 +1,14 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { useParams } from 'react-router-dom';
import { Environment, EnvironmentVariable } from 'gql-client';
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow';
import DisplayEnvironmentVariables from 'components/projects/project/settings/DisplayEnvironmentVariables';
import { useGQLClient } from 'context/GQLClientContext';
import { useGQLClient } from '../../../../../context/GQLClientContext';
import { EnvironmentVariablesFormValues } from '../../../../../types';
import HorizontalLine from 'components/HorizontalLine';
import { Heading } from 'components/shared/Heading';
@@ -15,13 +17,10 @@ import { Checkbox } from 'components/shared/Checkbox';
import { PlusIcon } from 'components/shared/CustomIcon';
import { InlineNotification } from 'components/shared/InlineNotification';
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
import { useToast } from 'components/shared/Toast';
import { Environment, EnvironmentVariable } from 'gql-client';
export const EnvironmentVariablesTabPanel = () => {
const { id } = useParams();
const client = useGQLClient();
const { toast, dismiss } = useToast();
const [environmentVariables, setEnvironmentVariables] = useState<
EnvironmentVariable[]
@@ -119,25 +118,13 @@ export const EnvironmentVariablesTabPanel = () => {
fetchEnvironmentVariables(id);
toast({
id:
createFormData.variables.length > 1
? 'env_variable_added'
: 'env_variables_added',
title:
createFormData.variables.length > 1
? `${createFormData.variables.length} variables added`
: `Variable added`,
variant: 'success',
onDismiss: dismiss,
});
toast.success(
createFormData.variables.length > 1
? `${createFormData.variables.length} variables added`
: `Variable added`,
);
} else {
toast({
id: 'env_variables_not_added',
title: 'Environment variables not added',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Environment variables not added');
}
},
[id, client],
@@ -1,12 +1,12 @@
import toast from 'react-hot-toast';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useGQLClient } from 'context/GQLClientContext';
import { useGQLClient } from '../../../../../../../context/GQLClientContext';
import { Table } from 'components/shared/Table';
import { Button } from 'components/shared/Button';
import { InlineNotification } from 'components/shared/InlineNotification';
import { ArrowRightCircleIcon } from 'components/shared/CustomIcon';
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
import { useToast } from 'components/shared/Toast';
const Config = () => {
const { id, orgSlug } = useParams();
@@ -14,26 +14,15 @@ const Config = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const primaryDomainName = searchParams.get('name');
const { toast, dismiss } = useToast();
const handleSubmitDomain = async () => {
if (primaryDomainName === null) {
toast({
id: 'unresolvable_domain_name',
title: 'Cannot resolve domain name',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Cannot resolve domain name');
return;
}
if (id === undefined) {
toast({
id: 'domain_cannot_find_project',
title: 'Cannot find project',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Cannot find project');
return;
}
@@ -42,20 +31,10 @@ const Config = () => {
});
if (isAdded) {
toast({
id: 'domain_added_successfully',
title: 'Domain added successfully',
variant: 'success',
onDismiss: dismiss,
});
toast.success('Domain added successfully');
navigate(`/${orgSlug}/projects/${id}/settings/domains`);
} else {
toast({
id: 'generic_error_adding_domain',
title: 'Error adding domaint',
variant: 'error',
onDismiss: dismiss,
});
toast.error('Error adding domain');
}
};
@@ -6,13 +6,9 @@ const meta: Meta<typeof ChevronDownSmallIcon> = {
title: 'Icons/ChevronDownSmallIcon',
component: ChevronDownSmallIcon,
tags: ['autodocs'],
argTypes: {
size: {
control: 'text',
},
name: {
control: 'text',
},
args: {
size: 'string | number' as unknown as any,
name: 'string',
},
};
@@ -6,13 +6,9 @@ const meta: Meta<typeof ChevronRight> = {
title: 'Icons/ChevronRight',
component: ChevronRight,
tags: ['autodocs'],
argTypes: {
size: {
control: 'text',
},
name: {
control: 'text',
},
args: {
size: 'string | number' as unknown as any,
name: 'string',
},
};
@@ -6,13 +6,9 @@ const meta: Meta<typeof ChevronUpDown> = {
title: 'Icons/ChevronUpDown',
component: ChevronUpDown,
tags: ['autodocs'],
argTypes: {
size: {
control: 'text',
},
name: {
control: 'text',
},
args: {
size: 'string | number' as unknown as any,
name: 'string',
},
};
@@ -3,16 +3,12 @@ import { Meta, StoryObj } from '@storybook/react';
import { ChevronUpSmallIcon } from 'components/shared/CustomIcon';
const meta: Meta<typeof ChevronUpSmallIcon> = {
title: 'Icons/ChevronUpSmallIcon',
title: 'Components/ChevronUpSmallIcon',
component: ChevronUpSmallIcon,
tags: ['autodocs'],
argTypes: {
size: {
control: 'text',
},
name: {
control: 'text',
},
args: {
size: 'string | number' as unknown as any,
name: 'string',
},
};
@@ -3,7 +3,7 @@ import { Meta, StoryObj } from '@storybook/react';
import { CollaboratorsIcon } from 'components/shared/CustomIcon';
const meta: Meta<typeof CollaboratorsIcon> = {
title: 'Icons/CollaboratorsIcon',
title: 'Components/CollaboratorsIcon',
component: CollaboratorsIcon,
tags: ['autodocs'],
argTypes: {
@@ -1,20 +0,0 @@
import { StoryObj, Meta } from '@storybook/react';
import { ProjectSearchBar } from 'components/projects/ProjectSearchBar';
const meta: Meta<typeof ProjectSearchBar> = {
title: 'Components/ProjectSearchBar',
component: ProjectSearchBar,
tags: ['autodocs'],
argTypes: {
onChange: {
action: 'change',
},
},
};
export default meta;
type Story = StoryObj<typeof ProjectSearchBar>;
export const Default: Story = {};
@@ -11,7 +11,6 @@ import {
DomainStatus,
Domain,
Environment,
Permission,
} from 'gql-client';
export const user: User = {
@@ -45,7 +44,7 @@ export const organization: Organization = {
export const member: ProjectMember = {
id: '1',
member: user,
permissions: [Permission.Edit],
permissions: [],
isPending: false,
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
@@ -71,7 +70,7 @@ export const environmentVariable1: EnvironmentVariable = {
export const domain0: Domain = {
id: '1',
name: 'domain.com',
name: 'Domain',
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
branch: 'Branch',
@@ -79,16 +78,6 @@ export const domain0: Domain = {
redirectTo: null,
};
export const domain1: Domain = {
id: '2',
name: 'www.domain.com',
createdAt: '2021-08-01T00:00:00.000Z',
updatedAt: '2021-08-01T00:00:00.000Z',
branch: 'Branch',
status: DomainStatus.Live,
redirectTo: domain0,
};
export const deployment0: Deployment = {
id: '1',
url: 'https://deployment.com',
@@ -102,7 +91,6 @@ export const deployment0: Deployment = {
domain: domain0,
commitMessage: 'Commit Message',
createdBy: user,
deployerLrn: 'lrn://deployer.apps.snowballtools.com ',
};
export const project: Project = {
@@ -120,9 +108,7 @@ export const project: Project = {
organization: organization,
template: 'Template',
members: [member],
auctionId: '7553538436710373822151221341b43f577e07b0525d083cc9b2de98890138a1',
deployerLrns: ['lrn://deployer.apps.snowballtools.com '],
webhooks: ['beepboop'],
icon: 'Icon',
baseDomains: ['baseDomain'],
subDomain: 'SubDomain',
};
@@ -1,48 +0,0 @@
import { StoryObj, Meta } from '@storybook/react';
import DomainCard from 'components/projects/project/settings/DomainCard';
import { domain0, domain1, project } from '../../MockStoriesData';
const meta: Meta<typeof DomainCard> = {
title: 'Project/Settings/DomainCard',
component: DomainCard,
tags: ['autodocs'],
argTypes: {
domains: {
control: {
type: 'object',
},
},
domain: {
control: {
type: 'object',
},
},
branches: {
control: {
type: 'object',
},
},
project: {
control: {
type: 'object',
},
},
onUpdate: {
action: 'update',
},
},
};
export default meta;
type Story = StoryObj<typeof DomainCard>;
export const Default: Story = {
args: {
domains: [domain0, domain1],
domain: domain0,
branches: ['main'],
project: project,
},
};
@@ -1,43 +0,0 @@
import { StoryObj, Meta } from '@storybook/react';
import EditDomainDialog from 'components/projects/project/settings/EditDomainDialog';
const meta: Meta<typeof EditDomainDialog> = {
title: 'Components/EditDomainDialog',
component: EditDomainDialog,
tags: ['autodocs'],
argTypes: {
domains: {
control: {
type: 'object',
},
},
open: {
control: {
type: 'boolean',
},
},
handleOpen: {
action: 'open',
},
domain: {
control: {
type: 'object',
},
},
branches: {
control: {
type: 'object',
},
},
onUpdate: {
action: 'update',
},
},
};
export default meta;
type Story = StoryObj<typeof EditDomainDialog>;
export const Default: Story = {};
@@ -1,29 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import WebhookCard from 'components/projects/project/settings/WebhookCard';
const meta: Meta<typeof WebhookCard> = {
title: 'Project/Settings/WebhookCard',
component: WebhookCard,
tags: ['autodocs'],
argTypes: {
webhookUrl: {
control: {
type: 'text',
},
},
onDelete: {
action: 'delete',
},
},
};
export default meta;
type Story = StoryObj<typeof WebhookCard>;
export const Default: Story = {
args: {
webhookUrl: 'https://api.retool.com',
},
};
-28
View File
@@ -1,28 +0,0 @@
import { baseUrl } from './constants';
export async function verifyAccessCode(
accesscode: string,
): Promise<boolean | null> {
const res = await fetch(`${baseUrl}/auth/accesscode`, {
method: 'POST',
body: JSON.stringify({
accesscode,
}),
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
});
// If API returns a non-empty 200, this is a valid access code.
if (res.status == 200) {
const isValid = (await res.json()).isValid as boolean;
console.log('isValid', isValid);
return isValid;
} else if (res.status === 204) {
return null;
} else {
throw new Error(
`Unexpected response from access code endpoint: ${res.status}: ${await res.text()}`,
);
}
}
-11
View File
@@ -1,11 +0,0 @@
export const baseUrl = import.meta.env.VITE_SERVER_URL;
export const PASSKEY_WALLET_RPID = import.meta.env.VITE_PASSKEY_WALLET_RPID!;
export const TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!;
export const VITE_GITHUB_PWA_TEMPLATE_REPO = import.meta.env
.VITE_GITHUB_PWA_TEMPLATE_REPO;
export const VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = import.meta.env
.VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO;
export const VITE_GITHUB_CLIENT_ID = import.meta.env.VITE_GITHUB_CLIENT_ID;
export const VITE_WALLET_CONNECT_ID = import.meta.env.VITE_WALLET_CONNECT_ID;
export const VITE_BUGSNAG_API_KEY = import.meta.env.VITE_BUGSNAG_API_KEY;
export const VITE_LIT_RELAY_API_KEY = import.meta.env.VITE_LIT_RELAY_API_KEY;
+8 -8
View File
@@ -1,21 +1,21 @@
import React from 'react';
import Bugsnag from '@bugsnag/js';
import BugsnagPluginReact from '@bugsnag/plugin-react';
import BugsnagPerformance from '@bugsnag/browser-performance';
import React from 'react';
import { VITE_BUGSNAG_API_KEY } from './constants';
const bugsnagApiKey = import.meta.env.VITE_BUGSNAG_API_KEY;
if (VITE_BUGSNAG_API_KEY) {
if (bugsnagApiKey) {
Bugsnag.start({
apiKey: VITE_BUGSNAG_API_KEY,
apiKey: bugsnagApiKey,
plugins: [new BugsnagPluginReact()],
});
BugsnagPerformance.start({ apiKey: VITE_BUGSNAG_API_KEY });
BugsnagPerformance.start({ apiKey: bugsnagApiKey });
}
export const errorLoggingEnabled = !!VITE_BUGSNAG_API_KEY;
export const errorLoggingEnabled = !!bugsnagApiKey;
export const LogErrorBoundary = VITE_BUGSNAG_API_KEY
export const LogErrorBoundary = bugsnagApiKey
? Bugsnag.getPlugin('react')!.createErrorBoundary(React)
: ({ children }: any) => children;
@@ -28,7 +28,7 @@ export function logError(error: Error) {
}
console.error(...errors);
if (VITE_BUGSNAG_API_KEY) {
if (import.meta.env.VITE_BUGSNAG_API_KEY) {
Bugsnag.notify(error);
}
}
+1 -3
View File
@@ -2,8 +2,6 @@ import { SiweMessage } from 'siwe';
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
import { v4 as uuid } from 'uuid';
import { baseUrl } from './constants';
const domain = window.location.host;
const origin = window.location.origin;
@@ -19,7 +17,7 @@ export async function signInWithEthereum(
);
const signature = await wallet.signMessage(message);
const res = await fetch(`${baseUrl}/auth/validate`, {
const res = await fetch(`${import.meta.env.VITE_SERVER_URL}/auth/validate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -1,7 +1,10 @@
import { TurnkeyClient, getWebAuthnAttestation } from '@turnkey/http';
import { WebauthnStamper } from '@turnkey/webauthn-stamper';
import { baseUrl, PASSKEY_WALLET_RPID, TURNKEY_BASE_URL } from './constants';
const baseUrl = import.meta.env.VITE_SERVER_URL;
const PASSKEY_WALLET_RPID = import.meta.env.VITE_PASSKEY_WALLET_RPID!;
const TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!;
// All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
// We only support ES256, which is listed here
+3 -4
View File
@@ -5,17 +5,16 @@ import {
LitGoogleAuth,
LitPasskeyAuth,
} from '@snowballtools/auth-lit';
import { VITE_LIT_RELAY_API_KEY } from './constants';
export const snowball = Snowball.withAuth({
google: LitGoogleAuth.configure({
litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!,
}),
// apple: LitAppleAuth.configure({
// litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
// litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!,
// }),
passkey: LitPasskeyAuth.configure({
litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
litRelayApiKey: import.meta.env.VITE_LIT_RELAY_API_KEY!,
}),
}).create({
initialChain: SnowballChain.sepolia,
+1 -5
View File
@@ -1,7 +1,5 @@
import { defineConfig, PluginOption } from 'vite';
import react from '@vitejs/plugin-react';
import { exec } from 'child_process';
import { promisify } from 'util';
// https://vitejs.dev/config/
export default defineConfig({
@@ -13,12 +11,10 @@ export default defineConfig({
context: '/src/context',
components: '/src/components',
pages: '/src/pages',
types: '/src/types',
},
},
define: {
'process.env': {},
__VERSION__: JSON.stringify(
(await promisify(exec)('git rev-parse --short HEAD')).stdout.trim(),
),
},
});

Some files were not shown because too many files have changed in this diff Show More