forked from cerc-io/snowballtools-base
Compare commits
1 Commits
main
...
06-22-load
Author | SHA1 | Date | |
---|---|---|---|
|
c7fd7c010d |
@ -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
|
|
@ -15,8 +15,8 @@ VITE_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_github_clientid'
|
|||||||
VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo'
|
VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo'
|
||||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo'
|
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo'
|
||||||
VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id'
|
VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id'
|
||||||
VITE_LACONICD_CHAIN_ID = 'LACONIC_HOSTED_CONFIG_laconicd_chain_id'
|
|
||||||
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
|
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
|
||||||
|
VITE_ALCHEMY_API_KEY = 'LACONIC_HOSTED_CONFIG_aplchemy_api_key'
|
||||||
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'
|
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'
|
||||||
VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid'
|
VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid'
|
||||||
VITE_TURNKEY_API_BASE_URL = 'LACONIC_HOSTED_CONFIG_turnkey_api_base_url'
|
VITE_TURNKEY_API_BASE_URL = 'LACONIC_HOSTED_CONFIG_turnkey_api_base_url'
|
||||||
@ -24,7 +24,7 @@ VITE_TURNKEY_ORGANIZATION_ID = 'LACONIC_HOSTED_CONFIG_turnkey_organization_id'
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
yarn || exit 1
|
yarn || exit 1
|
||||||
yarn build --ignore backend || exit 1
|
yarn build || exit 1
|
||||||
|
|
||||||
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
||||||
echo "Missing output directory: $OUTPUT_DIR" 1>&2
|
echo "Missing output directory: $OUTPUT_DIR" 1>&2
|
||||||
|
@ -22,10 +22,10 @@ yarn build --ignore frontend
|
|||||||
|
|
||||||
#### Local
|
#### Local
|
||||||
|
|
||||||
Copy the `environments/local.toml.example` file to `environments/local.toml`:
|
Copy the `envionments/local.toml.example` file to `envionments/local.toml`:
|
||||||
|
|
||||||
```zsh
|
```zsh
|
||||||
cp environments/local.toml.example environments/local.toml
|
cp envionments/local.toml.example envionments/local.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Staging environment variables
|
#### Staging environment variables
|
||||||
@ -44,7 +44,7 @@ yarn start
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
Clone the [deployer repository](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments):
|
Clone the deployer repository:
|
||||||
|
|
||||||
```zsh
|
```zsh
|
||||||
git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git
|
git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git
|
||||||
|
@ -3,12 +3,9 @@
|
|||||||
port = 8000
|
port = 8000
|
||||||
gqlPath = "/graphql"
|
gqlPath = "/graphql"
|
||||||
[server.session]
|
[server.session]
|
||||||
secret = ""
|
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi"
|
||||||
# Frontend webapp URL origin
|
|
||||||
appOriginUrl = "http://localhost:3000"
|
appOriginUrl = "http://localhost:3000"
|
||||||
# Set to true if server running behind proxy
|
|
||||||
trustProxy = false
|
trustProxy = false
|
||||||
# Backend URL hostname
|
|
||||||
domain = "localhost"
|
domain = "localhost"
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
@ -22,7 +19,6 @@
|
|||||||
|
|
||||||
[registryConfig]
|
[registryConfig]
|
||||||
fetchDeploymentRecordDelay = 5000
|
fetchDeploymentRecordDelay = 5000
|
||||||
checkAuctionStatusDelay = 5000
|
|
||||||
restEndpoint = "http://localhost:1317"
|
restEndpoint = "http://localhost:1317"
|
||||||
gqlEndpoint = "http://localhost:9473/api"
|
gqlEndpoint = "http://localhost:9473/api"
|
||||||
chainId = "laconic_9000-1"
|
chainId = "laconic_9000-1"
|
||||||
@ -30,14 +26,9 @@
|
|||||||
bondId = ""
|
bondId = ""
|
||||||
authority = ""
|
authority = ""
|
||||||
[registryConfig.fee]
|
[registryConfig.fee]
|
||||||
gas = ""
|
amount = "200000"
|
||||||
fees = ""
|
denom = "aphoton"
|
||||||
gasPrice = "1alnt"
|
gas = "750000"
|
||||||
|
|
||||||
# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions
|
[misc]
|
||||||
[auction]
|
projectDomain = "apps.snowballtools.com"
|
||||||
commitFee = "100000"
|
|
||||||
commitsDuration = "120s"
|
|
||||||
revealFee = "100000"
|
|
||||||
revealsDuration = "120s"
|
|
||||||
denom = "alnt"
|
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cerc-io/registry-sdk": "^0.2.11",
|
|
||||||
"@graphql-tools/schema": "^10.0.2",
|
"@graphql-tools/schema": "^10.0.2",
|
||||||
"@graphql-tools/utils": "^10.0.12",
|
"@graphql-tools/utils": "^10.0.12",
|
||||||
"@octokit/oauth-app": "^6.1.0",
|
"@octokit/oauth-app": "^6.1.0",
|
||||||
|
"@snowballtools/laconic-sdk": "^0.1.17",
|
||||||
"@turnkey/sdk-server": "^0.1.0",
|
"@turnkey/sdk-server": "^0.1.0",
|
||||||
"@types/debug": "^4.1.5",
|
"@types/debug": "^4.1.5",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
@ -34,21 +34,16 @@ export interface RegistryConfig {
|
|||||||
privateKey: string;
|
privateKey: string;
|
||||||
bondId: string;
|
bondId: string;
|
||||||
fetchDeploymentRecordDelay: number;
|
fetchDeploymentRecordDelay: number;
|
||||||
checkAuctionStatusDelay: number;
|
|
||||||
authority: string;
|
authority: string;
|
||||||
fee: {
|
fee: {
|
||||||
|
amount: string;
|
||||||
|
denom: string;
|
||||||
gas: string;
|
gas: string;
|
||||||
fees: string;
|
|
||||||
gasPrice: string;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuctionConfig {
|
export interface MiscConfig {
|
||||||
commitFee: string;
|
projectDomain: string;
|
||||||
commitsDuration: string;
|
|
||||||
revealFee: string;
|
|
||||||
revealsDuration: string;
|
|
||||||
denom: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
@ -56,7 +51,7 @@ export interface Config {
|
|||||||
database: DatabaseConfig;
|
database: DatabaseConfig;
|
||||||
gitHub: GitHubConfig;
|
gitHub: GitHubConfig;
|
||||||
registryConfig: RegistryConfig;
|
registryConfig: RegistryConfig;
|
||||||
auction: AuctionConfig;
|
misc: MiscConfig;
|
||||||
turnkey: {
|
turnkey: {
|
||||||
apiBaseUrl: string;
|
apiBaseUrl: string;
|
||||||
apiPublicKey: string;
|
apiPublicKey: string;
|
||||||
|
@ -3,9 +3,7 @@ import {
|
|||||||
DeepPartial,
|
DeepPartial,
|
||||||
FindManyOptions,
|
FindManyOptions,
|
||||||
FindOneOptions,
|
FindOneOptions,
|
||||||
FindOptionsWhere,
|
FindOptionsWhere
|
||||||
IsNull,
|
|
||||||
Not
|
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
@ -13,7 +11,7 @@ import assert from 'assert';
|
|||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
import { lowercase, numbers } from 'nanoid-dictionary';
|
import { lowercase, numbers } from 'nanoid-dictionary';
|
||||||
|
|
||||||
import { DatabaseConfig } from './config';
|
import { DatabaseConfig, MiscConfig } from './config';
|
||||||
import { User } from './entity/User';
|
import { User } from './entity/User';
|
||||||
import { Organization } from './entity/Organization';
|
import { Organization } from './entity/Organization';
|
||||||
import { Project } from './entity/Project';
|
import { Project } from './entity/Project';
|
||||||
@ -23,7 +21,6 @@ import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
|||||||
import { Domain } from './entity/Domain';
|
import { Domain } from './entity/Domain';
|
||||||
import { getEntities, loadAndSaveData } from './utils';
|
import { getEntities, loadAndSaveData } from './utils';
|
||||||
import { UserOrganization } from './entity/UserOrganization';
|
import { UserOrganization } from './entity/UserOrganization';
|
||||||
import { Deployer } from './entity/Deployer';
|
|
||||||
|
|
||||||
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
||||||
|
|
||||||
@ -34,8 +31,9 @@ const nanoid = customAlphabet(lowercase + numbers, 8);
|
|||||||
// TODO: Fix order of methods
|
// TODO: Fix order of methods
|
||||||
export class Database {
|
export class Database {
|
||||||
private dataSource: DataSource;
|
private dataSource: DataSource;
|
||||||
|
private projectDomain: string;
|
||||||
|
|
||||||
constructor({ dbPath }: DatabaseConfig) {
|
constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) {
|
||||||
this.dataSource = new DataSource({
|
this.dataSource = new DataSource({
|
||||||
type: 'better-sqlite3',
|
type: 'better-sqlite3',
|
||||||
database: dbPath,
|
database: dbPath,
|
||||||
@ -43,9 +41,11 @@ export class Database {
|
|||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: false
|
logging: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.projectDomain = projectDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init (): Promise<void> {
|
||||||
await this.dataSource.initialize();
|
await this.dataSource.initialize();
|
||||||
log('database initialized');
|
log('database initialized');
|
||||||
|
|
||||||
@ -58,21 +58,21 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser(options: FindOneOptions<User>): Promise<User | null> {
|
async getUser (options: FindOneOptions<User>): Promise<User | null> {
|
||||||
const userRepository = this.dataSource.getRepository(User);
|
const userRepository = this.dataSource.getRepository(User);
|
||||||
const user = await userRepository.findOne(options);
|
const user = await userRepository.findOne(options);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addUser(data: DeepPartial<User>): Promise<User> {
|
async addUser (data: DeepPartial<User>): Promise<User> {
|
||||||
const userRepository = this.dataSource.getRepository(User);
|
const userRepository = this.dataSource.getRepository(User);
|
||||||
const user = await userRepository.save(data);
|
const user = await userRepository.save(data);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser(user: User, data: DeepPartial<User>): Promise<boolean> {
|
async updateUser (user: User, data: DeepPartial<User>): Promise<boolean> {
|
||||||
const userRepository = this.dataSource.getRepository(User);
|
const userRepository = this.dataSource.getRepository(User);
|
||||||
const updateResult = await userRepository.update({ id: user.id }, data);
|
const updateResult = await userRepository.update({ id: user.id }, data);
|
||||||
assert(updateResult.affected);
|
assert(updateResult.affected);
|
||||||
@ -80,7 +80,7 @@ export class Database {
|
|||||||
return updateResult.affected > 0;
|
return updateResult.affected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganizations(
|
async getOrganizations (
|
||||||
options: FindManyOptions<Organization>
|
options: FindManyOptions<Organization>
|
||||||
): Promise<Organization[]> {
|
): Promise<Organization[]> {
|
||||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
@ -89,7 +89,7 @@ export class Database {
|
|||||||
return organizations;
|
return organizations;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganization(
|
async getOrganization (
|
||||||
options: FindOneOptions<Organization>
|
options: FindOneOptions<Organization>
|
||||||
): Promise<Organization | null> {
|
): Promise<Organization | null> {
|
||||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
@ -98,7 +98,7 @@ export class Database {
|
|||||||
return organization;
|
return organization;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrganizationsByUserId(userId: string): Promise<Organization[]> {
|
async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
|
||||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||||
|
|
||||||
const userOrgs = await organizationRepository.find({
|
const userOrgs = await organizationRepository.find({
|
||||||
@ -114,21 +114,21 @@ export class Database {
|
|||||||
return userOrgs;
|
return userOrgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addUserOrganization(data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
|
async addUserOrganization (data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
|
||||||
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
|
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
|
||||||
const newUserOrganization = await userOrganizationRepository.save(data);
|
const newUserOrganization = await userOrganizationRepository.save(data);
|
||||||
|
|
||||||
return newUserOrganization;
|
return newUserOrganization;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(options: FindManyOptions<Project>): Promise<Project[]> {
|
async getProjects (options: FindManyOptions<Project>): Promise<Project[]> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
const projects = await projectRepository.find(options);
|
const projects = await projectRepository.find(options);
|
||||||
|
|
||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectById(projectId: string): Promise<Project | null> {
|
async getProjectById (projectId: string): Promise<Project | null> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
const project = await projectRepository
|
const project = await projectRepository
|
||||||
@ -140,9 +140,7 @@ export class Database {
|
|||||||
)
|
)
|
||||||
.leftJoinAndSelect('deployments.createdBy', 'user')
|
.leftJoinAndSelect('deployments.createdBy', 'user')
|
||||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||||
.leftJoinAndSelect('deployments.deployer', 'deployer')
|
|
||||||
.leftJoinAndSelect('project.owner', 'owner')
|
.leftJoinAndSelect('project.owner', 'owner')
|
||||||
.leftJoinAndSelect('project.deployers', 'deployers')
|
|
||||||
.leftJoinAndSelect('project.organization', 'organization')
|
.leftJoinAndSelect('project.organization', 'organization')
|
||||||
.where('project.id = :projectId', {
|
.where('project.id = :projectId', {
|
||||||
projectId
|
projectId
|
||||||
@ -152,25 +150,7 @@ export class Database {
|
|||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
async allProjectsWithoutDeployments(): Promise<Project[]> {
|
async getProjectsInOrganization (
|
||||||
const allProjects = await this.getProjects({
|
|
||||||
where: {
|
|
||||||
auctionId: Not(IsNull()),
|
|
||||||
},
|
|
||||||
relations: ['deployments'],
|
|
||||||
withDeleted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const projects = allProjects.filter(project => {
|
|
||||||
if (project.deletedAt !== null) return false;
|
|
||||||
|
|
||||||
return project.deployments.length === 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return projects;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProjectsInOrganization(
|
|
||||||
userId: string,
|
userId: string,
|
||||||
organizationSlug: string
|
organizationSlug: string
|
||||||
): Promise<Project[]> {
|
): Promise<Project[]> {
|
||||||
@ -201,7 +181,7 @@ export class Database {
|
|||||||
/**
|
/**
|
||||||
* Get deployments with specified filter
|
* Get deployments with specified filter
|
||||||
*/
|
*/
|
||||||
async getDeployments(
|
async getDeployments (
|
||||||
options: FindManyOptions<Deployment>
|
options: FindManyOptions<Deployment>
|
||||||
): Promise<Deployment[]> {
|
): Promise<Deployment[]> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
@ -210,13 +190,12 @@ export class Database {
|
|||||||
return deployments;
|
return deployments;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeploymentsByProjectId(projectId: string): Promise<Deployment[]> {
|
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
|
||||||
return this.getDeployments({
|
return this.getDeployments({
|
||||||
relations: {
|
relations: {
|
||||||
project: true,
|
project: true,
|
||||||
domain: true,
|
domain: true,
|
||||||
createdBy: true,
|
createdBy: true
|
||||||
deployer: true,
|
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
project: {
|
project: {
|
||||||
@ -229,7 +208,7 @@ export class Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeployment(
|
async getDeployment (
|
||||||
options: FindOneOptions<Deployment>
|
options: FindOneOptions<Deployment>
|
||||||
): Promise<Deployment | null> {
|
): Promise<Deployment | null> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
@ -238,14 +217,14 @@ export class Database {
|
|||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDomains(options: FindManyOptions<Domain>): Promise<Domain[]> {
|
async getDomains (options: FindManyOptions<Domain>): Promise<Domain[]> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
const domains = await domainRepository.find(options);
|
const domains = await domainRepository.find(options);
|
||||||
|
|
||||||
return domains;
|
return domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDeployment(data: DeepPartial<Deployment>): Promise<Deployment> {
|
async addDeployment (data: DeepPartial<Deployment>): Promise<Deployment> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
|
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
@ -259,7 +238,7 @@ export class Database {
|
|||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMembersByProjectId(
|
async getProjectMembersByProjectId (
|
||||||
projectId: string
|
projectId: string
|
||||||
): Promise<ProjectMember[]> {
|
): Promise<ProjectMember[]> {
|
||||||
const projectMemberRepository =
|
const projectMemberRepository =
|
||||||
@ -280,7 +259,7 @@ export class Database {
|
|||||||
return projectMembers;
|
return projectMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEnvironmentVariablesByProjectId(
|
async getEnvironmentVariablesByProjectId (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
filter?: FindOptionsWhere<EnvironmentVariable>
|
filter?: FindOptionsWhere<EnvironmentVariable>
|
||||||
): Promise<EnvironmentVariable[]> {
|
): Promise<EnvironmentVariable[]> {
|
||||||
@ -299,7 +278,7 @@ export class Database {
|
|||||||
return environmentVariables;
|
return environmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeProjectMemberById(projectMemberId: string): Promise<boolean> {
|
async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
|
||||||
const projectMemberRepository =
|
const projectMemberRepository =
|
||||||
this.dataSource.getRepository(ProjectMember);
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
@ -314,7 +293,7 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProjectMemberById(
|
async updateProjectMemberById (
|
||||||
projectMemberId: string,
|
projectMemberId: string,
|
||||||
data: DeepPartial<ProjectMember>
|
data: DeepPartial<ProjectMember>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -328,7 +307,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProjectMember(
|
async addProjectMember (
|
||||||
data: DeepPartial<ProjectMember>
|
data: DeepPartial<ProjectMember>
|
||||||
): Promise<ProjectMember> {
|
): Promise<ProjectMember> {
|
||||||
const projectMemberRepository =
|
const projectMemberRepository =
|
||||||
@ -338,7 +317,7 @@ export class Database {
|
|||||||
return newProjectMember;
|
return newProjectMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addEnvironmentVariables(
|
async addEnvironmentVariables (
|
||||||
data: DeepPartial<EnvironmentVariable>[]
|
data: DeepPartial<EnvironmentVariable>[]
|
||||||
): Promise<EnvironmentVariable[]> {
|
): Promise<EnvironmentVariable[]> {
|
||||||
const environmentVariableRepository =
|
const environmentVariableRepository =
|
||||||
@ -349,7 +328,7 @@ export class Database {
|
|||||||
return savedEnvironmentVariables;
|
return savedEnvironmentVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEnvironmentVariable(
|
async updateEnvironmentVariable (
|
||||||
environmentVariableId: string,
|
environmentVariableId: string,
|
||||||
data: DeepPartial<EnvironmentVariable>
|
data: DeepPartial<EnvironmentVariable>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -363,7 +342,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteEnvironmentVariable(
|
async deleteEnvironmentVariable (
|
||||||
environmentVariableId: string
|
environmentVariableId: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const environmentVariableRepository =
|
const environmentVariableRepository =
|
||||||
@ -379,7 +358,7 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectMemberById(projectMemberId: string): Promise<ProjectMember> {
|
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
|
||||||
const projectMemberRepository =
|
const projectMemberRepository =
|
||||||
this.dataSource.getRepository(ProjectMember);
|
this.dataSource.getRepository(ProjectMember);
|
||||||
|
|
||||||
@ -402,7 +381,7 @@ export class Database {
|
|||||||
return projectMemberWithProject[0];
|
return projectMemberWithProject[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectsBySearchText(
|
async getProjectsBySearchText (
|
||||||
userId: string,
|
userId: string,
|
||||||
searchText: string
|
searchText: string
|
||||||
): Promise<Project[]> {
|
): Promise<Project[]> {
|
||||||
@ -424,14 +403,14 @@ export class Database {
|
|||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentById(
|
async updateDeploymentById (
|
||||||
deploymentId: string,
|
deploymentId: string,
|
||||||
data: DeepPartial<Deployment>
|
data: DeepPartial<Deployment>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
return this.updateDeployment({ id: deploymentId }, data);
|
return this.updateDeployment({ id: deploymentId }, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeployment(
|
async updateDeployment (
|
||||||
criteria: FindOptionsWhere<Deployment>,
|
criteria: FindOptionsWhere<Deployment>,
|
||||||
data: DeepPartial<Deployment>
|
data: DeepPartial<Deployment>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -441,7 +420,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDeploymentsByProjectIds(
|
async updateDeploymentsByProjectIds (
|
||||||
projectIds: string[],
|
projectIds: string[],
|
||||||
data: DeepPartial<Deployment>
|
data: DeepPartial<Deployment>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -457,7 +436,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteDeploymentById(deploymentId: string): Promise<boolean> {
|
async deleteDeploymentById (deploymentId: string): Promise<boolean> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
const deployment = await deploymentRepository.findOneOrFail({
|
const deployment = await deploymentRepository.findOneOrFail({
|
||||||
where: {
|
where: {
|
||||||
@ -470,7 +449,7 @@ export class Database {
|
|||||||
return Boolean(deleteResult);
|
return Boolean(deleteResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProject(user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
|
async addProject (user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
// TODO: Check if organization exists
|
// TODO: Check if organization exists
|
||||||
@ -486,16 +465,12 @@ export class Database {
|
|||||||
id: organizationId
|
id: organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
|
||||||
|
|
||||||
return projectRepository.save(newProject);
|
return projectRepository.save(newProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveProject(project: Project): Promise<Project> {
|
async updateProjectById (
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
|
||||||
|
|
||||||
return projectRepository.save(project);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateProjectById(
|
|
||||||
projectId: string,
|
projectId: string,
|
||||||
data: DeepPartial<Project>
|
data: DeepPartial<Project>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -508,7 +483,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteProjectById(projectId: string): Promise<boolean> {
|
async deleteProjectById (projectId: string): Promise<boolean> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
const project = await projectRepository.findOneOrFail({
|
const project = await projectRepository.findOneOrFail({
|
||||||
where: {
|
where: {
|
||||||
@ -524,7 +499,7 @@ export class Database {
|
|||||||
return Boolean(deleteResult);
|
return Boolean(deleteResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteDomainById(domainId: string): Promise<boolean> {
|
async deleteDomainById (domainId: string): Promise<boolean> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
|
|
||||||
const deleteResult = await domainRepository.softDelete({ id: domainId });
|
const deleteResult = await domainRepository.softDelete({ id: domainId });
|
||||||
@ -536,21 +511,21 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDomain(data: DeepPartial<Domain>): Promise<Domain> {
|
async addDomain (data: DeepPartial<Domain>): Promise<Domain> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
const newDomain = await domainRepository.save(data);
|
const newDomain = await domainRepository.save(data);
|
||||||
|
|
||||||
return newDomain;
|
return newDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDomain(options: FindOneOptions<Domain>): Promise<Domain | null> {
|
async getDomain (options: FindOneOptions<Domain>): Promise<Domain | null> {
|
||||||
const domainRepository = this.dataSource.getRepository(Domain);
|
const domainRepository = this.dataSource.getRepository(Domain);
|
||||||
const domain = await domainRepository.findOne(options);
|
const domain = await domainRepository.findOne(options);
|
||||||
|
|
||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDomainById(
|
async updateDomainById (
|
||||||
domainId: string,
|
domainId: string,
|
||||||
data: DeepPartial<Domain>
|
data: DeepPartial<Domain>
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@ -560,7 +535,7 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDomainsByProjectId(
|
async getDomainsByProjectId (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
filter?: FindOptionsWhere<Domain>
|
filter?: FindOptionsWhere<Domain>
|
||||||
): Promise<Domain[]> {
|
): Promise<Domain[]> {
|
||||||
@ -580,24 +555,4 @@ export class Database {
|
|||||||
|
|
||||||
return domains;
|
return domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDeployer(data: DeepPartial<Deployer>): Promise<Deployer> {
|
|
||||||
const deployerRepository = this.dataSource.getRepository(Deployer);
|
|
||||||
const newDomain = await deployerRepository.save(data);
|
|
||||||
|
|
||||||
return newDomain;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDeployers(): Promise<Deployer[]> {
|
|
||||||
const deployerRepository = this.dataSource.getRepository(Deployer);
|
|
||||||
const deployers = await deployerRepository.find();
|
|
||||||
return deployers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDeployerByLRN(deployerLrn: string): Promise<Deployer | null> {
|
|
||||||
const deployerRepository = this.dataSource.getRepository(Deployer);
|
|
||||||
const deployer = await deployerRepository.findOne({ where: { deployerLrn } });
|
|
||||||
|
|
||||||
return deployer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
import { Entity, PrimaryColumn, Column, ManyToMany } from 'typeorm';
|
|
||||||
import { Project } from './Project';
|
|
||||||
|
|
||||||
@Entity()
|
|
||||||
export class Deployer {
|
|
||||||
@PrimaryColumn('varchar')
|
|
||||||
deployerLrn!: string;
|
|
||||||
|
|
||||||
@Column('varchar')
|
|
||||||
deployerId!: string;
|
|
||||||
|
|
||||||
@Column('varchar')
|
|
||||||
deployerApiUrl!: string;
|
|
||||||
|
|
||||||
@Column('varchar')
|
|
||||||
baseDomain!: string;
|
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
|
||||||
minimumPayment!: string | null;
|
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
|
||||||
paymentAddress!: string | null;
|
|
||||||
|
|
||||||
@ManyToMany(() => Project, (project) => project.deployers)
|
|
||||||
projects!: Project[];
|
|
||||||
}
|
|
@ -13,7 +13,6 @@ import {
|
|||||||
import { Project } from './Project';
|
import { Project } from './Project';
|
||||||
import { Domain } from './Domain';
|
import { Domain } from './Domain';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
import { Deployer } from './Deployer';
|
|
||||||
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types';
|
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types';
|
||||||
|
|
||||||
export enum Environment {
|
export enum Environment {
|
||||||
@ -34,21 +33,21 @@ export interface ApplicationDeploymentRequest {
|
|||||||
version: string;
|
version: string;
|
||||||
name: string;
|
name: string;
|
||||||
application: string;
|
application: string;
|
||||||
lrn?: string;
|
|
||||||
auction?: string;
|
|
||||||
config: string;
|
config: string;
|
||||||
meta: string;
|
meta: string;
|
||||||
payment?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApplicationDeploymentRemovalRequest {
|
export interface ApplicationDeploymentRemovalRequest {
|
||||||
type: string;
|
type: string;
|
||||||
version: string;
|
version: string;
|
||||||
deployment: string;
|
deployment: string;
|
||||||
auction?: string;
|
|
||||||
payment?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApplicationDeploymentRemovalRequest {
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
deployment: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApplicationRecord {
|
export interface ApplicationRecord {
|
||||||
type: string;
|
type: string;
|
||||||
@ -126,10 +125,6 @@ export class Deployment {
|
|||||||
@Column('simple-json', { nullable: true })
|
@Column('simple-json', { nullable: true })
|
||||||
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
|
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
|
||||||
|
|
||||||
@ManyToOne(() => Deployer)
|
|
||||||
@JoinColumn({ name: 'deployerLrn' })
|
|
||||||
deployer!: Deployer;
|
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
enum: Environment
|
enum: Environment
|
||||||
})
|
})
|
||||||
|
@ -7,16 +7,13 @@ import {
|
|||||||
ManyToOne,
|
ManyToOne,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn
|
||||||
JoinTable,
|
|
||||||
ManyToMany
|
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
import { Organization } from './Organization';
|
import { Organization } from './Organization';
|
||||||
import { ProjectMember } from './ProjectMember';
|
import { ProjectMember } from './ProjectMember';
|
||||||
import { Deployment } from './Deployment';
|
import { Deployment } from './Deployment';
|
||||||
import { Deployer } from './Deployer';
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Project {
|
export class Project {
|
||||||
@ -49,20 +46,6 @@ export class Project {
|
|||||||
@Column('text', { default: '' })
|
@Column('text', { default: '' })
|
||||||
description!: string;
|
description!: string;
|
||||||
|
|
||||||
@Column('varchar', { nullable: true })
|
|
||||||
auctionId!: string | null;
|
|
||||||
|
|
||||||
// Tx hash for sending coins from snowball to deployer
|
|
||||||
@Column('varchar', { nullable: true })
|
|
||||||
txHash!: string | null;
|
|
||||||
|
|
||||||
@ManyToMany(() => Deployer, (deployer) => (deployer.projects))
|
|
||||||
@JoinTable()
|
|
||||||
deployers!: Deployer[]
|
|
||||||
|
|
||||||
@Column('boolean', { default: false, nullable: true })
|
|
||||||
fundsReleased!: boolean;
|
|
||||||
|
|
||||||
// TODO: Compute template & framework in import repository
|
// TODO: Compute template & framework in import repository
|
||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
template!: string | null;
|
template!: string | null;
|
||||||
@ -70,10 +53,6 @@ export class Project {
|
|||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
framework!: string | null;
|
framework!: string | null;
|
||||||
|
|
||||||
// Address of the user who created the project i.e. requested deployments
|
|
||||||
@Column('varchar')
|
|
||||||
paymentAddress!: string;
|
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: 'simple-array'
|
type: 'simple-array'
|
||||||
})
|
})
|
||||||
@ -82,6 +61,9 @@ export class Project {
|
|||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
icon!: string;
|
icon!: string;
|
||||||
|
|
||||||
|
@Column('varchar')
|
||||||
|
subDomain!: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ const log = debug('snowball:server');
|
|||||||
const OAUTH_CLIENT_TYPE = 'oauth-app';
|
const OAUTH_CLIENT_TYPE = 'oauth-app';
|
||||||
|
|
||||||
export const main = async (): Promise<void> => {
|
export const main = async (): Promise<void> => {
|
||||||
const { server, database, gitHub, registryConfig } = await getConfig();
|
const { server, database, gitHub, registryConfig, misc } = await getConfig();
|
||||||
|
|
||||||
const app = new OAuthApp({
|
const app = new OAuthApp({
|
||||||
clientType: OAUTH_CLIENT_TYPE,
|
clientType: OAUTH_CLIENT_TYPE,
|
||||||
@ -25,7 +25,7 @@ export const main = async (): Promise<void> => {
|
|||||||
clientSecret: gitHub.oAuth.clientSecret,
|
clientSecret: gitHub.oAuth.clientSecret,
|
||||||
});
|
});
|
||||||
|
|
||||||
const db = new Database(database);
|
const db = new Database(database, misc);
|
||||||
await db.init();
|
await db.init();
|
||||||
|
|
||||||
const registry = new Registry(registryConfig);
|
const registry = new Registry(registryConfig);
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import assert from 'assert';
|
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { DateTime } from 'luxon';
|
import assert from 'assert';
|
||||||
import { Octokit } from 'octokit';
|
|
||||||
import { inc as semverInc } from 'semver';
|
import { inc as semverInc } from 'semver';
|
||||||
import { DeepPartial } from 'typeorm';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { Account, DEFAULT_GAS_ESTIMATION_MULTIPLIER, Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk';
|
import { Registry as LaconicRegistry } from '@snowballtools/laconic-sdk';
|
||||||
import { DeliverTxResponse, IndexedTx } from '@cosmjs/stargate';
|
|
||||||
|
|
||||||
import { RegistryConfig } from './config';
|
import { RegistryConfig } from './config';
|
||||||
import {
|
import {
|
||||||
@ -15,53 +12,49 @@ import {
|
|||||||
ApplicationDeploymentRequest,
|
ApplicationDeploymentRequest,
|
||||||
ApplicationDeploymentRemovalRequest
|
ApplicationDeploymentRemovalRequest
|
||||||
} from './entity/Deployment';
|
} from './entity/Deployment';
|
||||||
import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord } from './types';
|
import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types';
|
||||||
import { getConfig, getRepoDetails, registryTransactionWithRetry, sleep } from './utils';
|
import { sleep } from './utils';
|
||||||
|
|
||||||
const log = debug('snowball:registry');
|
const log = debug('snowball:registry');
|
||||||
|
|
||||||
const APP_RECORD_TYPE = 'ApplicationRecord';
|
const APP_RECORD_TYPE = 'ApplicationRecord';
|
||||||
const APP_DEPLOYMENT_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction';
|
|
||||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
||||||
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
|
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
|
||||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
||||||
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
|
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
|
||||||
const WEBAPP_DEPLOYER_RECORD_TYPE = 'WebappDeployer'
|
|
||||||
const SLEEP_DURATION = 1000;
|
const SLEEP_DURATION = 1000;
|
||||||
|
|
||||||
// TODO: Move registry code to registry-sdk/watcher-ts
|
// TODO: Move registry code to laconic-sdk/watcher-ts
|
||||||
export class Registry {
|
export class Registry {
|
||||||
private registry: LaconicRegistry;
|
private registry: LaconicRegistry;
|
||||||
private registryConfig: RegistryConfig;
|
private registryConfig: RegistryConfig;
|
||||||
|
|
||||||
constructor(registryConfig: RegistryConfig) {
|
constructor (registryConfig: RegistryConfig) {
|
||||||
this.registryConfig = registryConfig;
|
this.registryConfig = registryConfig;
|
||||||
|
|
||||||
const gasPrice = getGasPrice(registryConfig.fee.gasPrice);
|
|
||||||
|
|
||||||
this.registry = new LaconicRegistry(
|
this.registry = new LaconicRegistry(
|
||||||
registryConfig.gqlEndpoint,
|
registryConfig.gqlEndpoint,
|
||||||
registryConfig.restEndpoint,
|
registryConfig.restEndpoint,
|
||||||
{ chainId: registryConfig.chainId, gasPrice }
|
registryConfig.chainId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationRecord({
|
async createApplicationRecord ({
|
||||||
octokit,
|
appName,
|
||||||
repository,
|
packageJSON,
|
||||||
commitHash,
|
commitHash,
|
||||||
appType,
|
appType,
|
||||||
|
repoUrl
|
||||||
}: {
|
}: {
|
||||||
octokit: Octokit
|
appName: string;
|
||||||
repository: string;
|
packageJSON: PackageJSON;
|
||||||
commitHash: string;
|
commitHash: string;
|
||||||
appType: string;
|
appType: string;
|
||||||
|
repoUrl: string;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
applicationRecordId: string;
|
applicationRecordId: string;
|
||||||
applicationRecordData: ApplicationRecord;
|
applicationRecordData: ApplicationRecord;
|
||||||
}> {
|
}> {
|
||||||
const { repo, repoUrl, packageJSON } = await getRepoDetails(octokit, repository, commitHash)
|
// Use laconic-sdk to publish record
|
||||||
// Use registry-sdk to publish record
|
|
||||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||||
// Fetch previous records
|
// Fetch previous records
|
||||||
const records = await this.registry.queryRecords(
|
const records = await this.registry.queryRecords(
|
||||||
@ -94,7 +87,7 @@ export class Registry {
|
|||||||
repository_ref: commitHash,
|
repository_ref: commitHash,
|
||||||
repository: [repoUrl],
|
repository: [repoUrl],
|
||||||
app_type: appType,
|
app_type: appType,
|
||||||
name: repo,
|
name: appName,
|
||||||
...(packageJSON.description && { description: packageJSON.description }),
|
...(packageJSON.description && { description: packageJSON.description }),
|
||||||
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||||
...(packageJSON.license && { license: packageJSON.license }),
|
...(packageJSON.license && { license: packageJSON.license }),
|
||||||
@ -107,158 +100,68 @@ export class Registry {
|
|||||||
...(packageJSON.version && { app_version: packageJSON.version })
|
...(packageJSON.version && { app_version: packageJSON.version })
|
||||||
};
|
};
|
||||||
|
|
||||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
const result = await this.registry.setRecord(
|
||||||
|
{
|
||||||
const result = await registryTransactionWithRetry(() =>
|
privateKey: this.registryConfig.privateKey,
|
||||||
this.registry.setRecord(
|
record: applicationRecord,
|
||||||
{
|
bondId: this.registryConfig.bondId
|
||||||
privateKey: this.registryConfig.privateKey,
|
},
|
||||||
record: applicationRecord,
|
'',
|
||||||
bondId: this.registryConfig.bondId
|
this.registryConfig.fee
|
||||||
},
|
|
||||||
this.registryConfig.privateKey,
|
|
||||||
fee
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
log(`Published application record ${result.id}`);
|
|
||||||
log('Application record data:', applicationRecord);
|
log('Application record data:', applicationRecord);
|
||||||
|
|
||||||
// TODO: Discuss computation of LRN
|
// TODO: Discuss computation of CRN
|
||||||
const lrn = this.getLrn(repo);
|
const crn = this.getCrn(appName);
|
||||||
log(`Setting name: ${lrn} for record ID: ${result.id}`);
|
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
await sleep(SLEEP_DURATION);
|
||||||
await registryTransactionWithRetry(() =>
|
await this.registry.setName(
|
||||||
this.registry.setName(
|
{ cid: result.data.id, crn },
|
||||||
{
|
this.registryConfig.privateKey,
|
||||||
cid: result.id,
|
this.registryConfig.fee
|
||||||
lrn
|
|
||||||
},
|
|
||||||
this.registryConfig.privateKey,
|
|
||||||
fee
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
await sleep(SLEEP_DURATION);
|
||||||
await registryTransactionWithRetry(() =>
|
await this.registry.setName(
|
||||||
this.registry.setName(
|
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
||||||
{
|
this.registryConfig.privateKey,
|
||||||
cid: result.id,
|
this.registryConfig.fee
|
||||||
lrn: `${lrn}@${applicationRecord.app_version}`
|
|
||||||
},
|
|
||||||
this.registryConfig.privateKey,
|
|
||||||
fee
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
await sleep(SLEEP_DURATION);
|
||||||
await registryTransactionWithRetry(() =>
|
await this.registry.setName(
|
||||||
this.registry.setName(
|
{
|
||||||
{
|
cid: result.data.id,
|
||||||
cid: result.id,
|
crn: `${crn}@${applicationRecord.repository_ref}`
|
||||||
lrn: `${lrn}@${applicationRecord.repository_ref}`
|
},
|
||||||
},
|
this.registryConfig.privateKey,
|
||||||
this.registryConfig.privateKey,
|
this.registryConfig.fee
|
||||||
fee
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
applicationRecordId: result.id,
|
applicationRecordId: result.data.id,
|
||||||
applicationRecordData: applicationRecord
|
applicationRecordData: applicationRecord
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationDeploymentAuction(
|
async createApplicationDeploymentRequest (data: {
|
||||||
appName: string,
|
|
||||||
octokit: Octokit,
|
|
||||||
auctionParams: AuctionParams,
|
|
||||||
data: DeepPartial<Deployment>,
|
|
||||||
): Promise<{
|
|
||||||
applicationDeploymentAuctionId: string;
|
|
||||||
}> {
|
|
||||||
assert(data.project?.repository, 'Project repository not found');
|
|
||||||
|
|
||||||
await this.createApplicationRecord({
|
|
||||||
octokit,
|
|
||||||
repository: data.project.repository,
|
|
||||||
appType: data.project!.template!,
|
|
||||||
commitHash: data.commitHash!,
|
|
||||||
});
|
|
||||||
|
|
||||||
const lrn = this.getLrn(appName);
|
|
||||||
const config = await getConfig();
|
|
||||||
const auctionConfig = config.auction;
|
|
||||||
|
|
||||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
|
||||||
const auctionResult = await registryTransactionWithRetry(() =>
|
|
||||||
this.registry.createProviderAuction(
|
|
||||||
{
|
|
||||||
commitFee: auctionConfig.commitFee,
|
|
||||||
commitsDuration: auctionConfig.commitsDuration,
|
|
||||||
revealFee: auctionConfig.revealFee,
|
|
||||||
revealsDuration: auctionConfig.revealsDuration,
|
|
||||||
denom: auctionConfig.denom,
|
|
||||||
maxPrice: auctionParams.maxPrice,
|
|
||||||
numProviders: auctionParams.numProviders,
|
|
||||||
},
|
|
||||||
this.registryConfig.privateKey,
|
|
||||||
fee
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!auctionResult.auction) {
|
|
||||||
throw new Error('Error creating auction');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create record of type applicationDeploymentAuction and publish
|
|
||||||
const applicationDeploymentAuction = {
|
|
||||||
application: lrn,
|
|
||||||
auction: auctionResult.auction.id,
|
|
||||||
type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await registryTransactionWithRetry(() =>
|
|
||||||
this.registry.setRecord(
|
|
||||||
{
|
|
||||||
privateKey: this.registryConfig.privateKey,
|
|
||||||
record: applicationDeploymentAuction,
|
|
||||||
bondId: this.registryConfig.bondId
|
|
||||||
},
|
|
||||||
this.registryConfig.privateKey,
|
|
||||||
fee
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
log(`Application deployment auction created: ${auctionResult.auction.id}`);
|
|
||||||
log(`Application deployment auction record published: ${result.id}`);
|
|
||||||
log('Application deployment auction data:', applicationDeploymentAuction);
|
|
||||||
|
|
||||||
return {
|
|
||||||
applicationDeploymentAuctionId: auctionResult.auction.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async createApplicationDeploymentRequest(data: {
|
|
||||||
deployment: Deployment,
|
deployment: Deployment,
|
||||||
appName: string,
|
appName: string,
|
||||||
repository: string,
|
repository: string,
|
||||||
auctionId?: string | null,
|
|
||||||
lrn: string,
|
|
||||||
environmentVariables: { [key: string]: string },
|
environmentVariables: { [key: string]: string },
|
||||||
dns: string,
|
dns: string,
|
||||||
payment?: string | null
|
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
applicationDeploymentRequestId: string;
|
applicationDeploymentRequestId: string;
|
||||||
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||||
}> {
|
}> {
|
||||||
const lrn = this.getLrn(data.appName);
|
const crn = this.getCrn(data.appName);
|
||||||
const records = await this.registry.resolveNames([lrn]);
|
const records = await this.registry.resolveNames([crn]);
|
||||||
const applicationRecord = records[0];
|
const applicationRecord = records[0];
|
||||||
|
|
||||||
if (!applicationRecord) {
|
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
|
// Create record of type ApplicationDeploymentRequest and publish
|
||||||
@ -266,9 +169,12 @@ export class Registry {
|
|||||||
type: APP_DEPLOYMENT_REQUEST_TYPE,
|
type: APP_DEPLOYMENT_REQUEST_TYPE,
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
||||||
application: `${lrn}@${applicationRecord.attributes.app_version}`,
|
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
||||||
dns: data.dns,
|
dns: data.dns,
|
||||||
|
|
||||||
|
// TODO: Not set in test-progressive-web-app CI
|
||||||
|
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
||||||
|
|
||||||
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
||||||
config: JSON.stringify({
|
config: JSON.stringify({
|
||||||
env: data.environmentVariables
|
env: data.environmentVariables
|
||||||
@ -279,87 +185,32 @@ export class Registry {
|
|||||||
)}`,
|
)}`,
|
||||||
repository: data.repository,
|
repository: data.repository,
|
||||||
repository_ref: data.deployment.commitHash
|
repository_ref: data.deployment.commitHash
|
||||||
}),
|
})
|
||||||
deployer: data.lrn,
|
|
||||||
...(data.auctionId && { auction: data.auctionId }),
|
|
||||||
...(data.payment && { payment: data.payment }),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await sleep(SLEEP_DURATION);
|
await sleep(SLEEP_DURATION);
|
||||||
|
const result = await this.registry.setRecord(
|
||||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
{
|
||||||
|
privateKey: this.registryConfig.privateKey,
|
||||||
const result = await registryTransactionWithRetry(() =>
|
record: applicationDeploymentRequest,
|
||||||
this.registry.setRecord(
|
bondId: this.registryConfig.bondId
|
||||||
{
|
},
|
||||||
privateKey: this.registryConfig.privateKey,
|
'',
|
||||||
record: applicationDeploymentRequest,
|
this.registryConfig.fee
|
||||||
bondId: this.registryConfig.bondId
|
|
||||||
},
|
|
||||||
this.registryConfig.privateKey,
|
|
||||||
fee
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
log(`Application deployment request record published: ${result.data.id}`);
|
||||||
log(`Application deployment request record published: ${result.id}`);
|
|
||||||
log('Application deployment request data:', applicationDeploymentRequest);
|
log('Application deployment request data:', applicationDeploymentRequest);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
applicationDeploymentRequestId: result.id,
|
applicationDeploymentRequestId: result.data.id,
|
||||||
applicationDeploymentRequestData: applicationDeploymentRequest
|
applicationDeploymentRequestData: applicationDeploymentRequest
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAuctionWinningDeployerRecords(
|
|
||||||
auctionId: string
|
|
||||||
): Promise<DeployerRecord[]> {
|
|
||||||
const records = await this.registry.getAuctionsByIds([auctionId]);
|
|
||||||
const auctionResult = records[0];
|
|
||||||
|
|
||||||
let deployerRecords = [];
|
|
||||||
const { winnerAddresses } = auctionResult;
|
|
||||||
|
|
||||||
for (const auctionWinner of winnerAddresses) {
|
|
||||||
const records = await this.getDeployerRecordsByFilter({
|
|
||||||
paymentAddress: auctionWinner,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newRecords = records.filter(record => {
|
|
||||||
return record.names !== null && record.names.length > 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const record of newRecords) {
|
|
||||||
if (record.id) {
|
|
||||||
deployerRecords.push(record);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return deployerRecords;
|
|
||||||
}
|
|
||||||
|
|
||||||
async releaseDeployerFunds(
|
|
||||||
auctionId: string
|
|
||||||
): Promise<any> {
|
|
||||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
|
||||||
const auction = await registryTransactionWithRetry(() =>
|
|
||||||
this.registry.releaseFunds(
|
|
||||||
{
|
|
||||||
auctionId
|
|
||||||
},
|
|
||||||
this.registryConfig.privateKey,
|
|
||||||
fee
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return auction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch ApplicationDeploymentRecords for deployments
|
* Fetch ApplicationDeploymentRecords for deployments
|
||||||
*/
|
*/
|
||||||
async getDeploymentRecords(
|
async getDeploymentRecords (
|
||||||
deployments: Deployment[]
|
deployments: Deployment[]
|
||||||
): Promise<AppDeploymentRecord[]> {
|
): Promise<AppDeploymentRecord[]> {
|
||||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
||||||
@ -371,33 +222,20 @@ export class Registry {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter records with ApplicationDeploymentRequestId ID and Deployment specific URL
|
// Filter records with ApplicationRecord ID and Deployment specific URL
|
||||||
return records.filter((record: AppDeploymentRecord) =>
|
return records.filter((record: AppDeploymentRecord) =>
|
||||||
deployments.some(
|
deployments.some(
|
||||||
(deployment) =>
|
(deployment) =>
|
||||||
deployment.applicationDeploymentRequestId === record.attributes.request &&
|
deployment.applicationRecordId === record.attributes.application &&
|
||||||
record.attributes.url.includes(deployment.id)
|
record.attributes.url.includes(deployment.id)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch WebappDeployer Records by filter
|
|
||||||
*/
|
|
||||||
async getDeployerRecordsByFilter(filter: { [key: string]: any }): Promise<DeployerRecord[]> {
|
|
||||||
return this.registry.queryRecords(
|
|
||||||
{
|
|
||||||
type: WEBAPP_DEPLOYER_RECORD_TYPE,
|
|
||||||
...filter
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch ApplicationDeploymentRecords by filter
|
* Fetch ApplicationDeploymentRecords by filter
|
||||||
*/
|
*/
|
||||||
async getDeploymentRecordsByFilter(filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
||||||
return this.registry.queryRecords(
|
return this.registry.queryRecords(
|
||||||
{
|
{
|
||||||
type: APP_DEPLOYMENT_RECORD_TYPE,
|
type: APP_DEPLOYMENT_RECORD_TYPE,
|
||||||
@ -410,7 +248,7 @@ export class Registry {
|
|||||||
/**
|
/**
|
||||||
* Fetch ApplicationDeploymentRemovalRecords for deployments
|
* Fetch ApplicationDeploymentRemovalRecords for deployments
|
||||||
*/
|
*/
|
||||||
async getDeploymentRemovalRecords(
|
async getDeploymentRemovalRecords (
|
||||||
deployments: Deployment[]
|
deployments: Deployment[]
|
||||||
): Promise<AppDeploymentRemovalRecord[]> {
|
): Promise<AppDeploymentRemovalRecord[]> {
|
||||||
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
|
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
|
||||||
@ -431,11 +269,8 @@ export class Registry {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplicationDeploymentRemovalRequest(data: {
|
async createApplicationDeploymentRemovalRequest (data: {
|
||||||
deploymentId: string;
|
deploymentId: string;
|
||||||
deployerLrn: string;
|
|
||||||
auctionId?: string | null;
|
|
||||||
payment?: string | null;
|
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
applicationDeploymentRemovalRequestId: string;
|
applicationDeploymentRemovalRequestId: string;
|
||||||
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
||||||
@ -443,93 +278,30 @@ export class Registry {
|
|||||||
const applicationDeploymentRemovalRequest = {
|
const applicationDeploymentRemovalRequest = {
|
||||||
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
|
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
deployment: data.deploymentId,
|
deployment: data.deploymentId
|
||||||
deployer: data.deployerLrn,
|
|
||||||
...(data.auctionId && { auction: data.auctionId }),
|
|
||||||
...(data.payment && { payment: data.payment }),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
const result = await this.registry.setRecord(
|
||||||
|
{
|
||||||
const result = await registryTransactionWithRetry(() =>
|
privateKey: this.registryConfig.privateKey,
|
||||||
this.registry.setRecord(
|
record: applicationDeploymentRemovalRequest,
|
||||||
{
|
bondId: this.registryConfig.bondId
|
||||||
privateKey: this.registryConfig.privateKey,
|
},
|
||||||
record: applicationDeploymentRemovalRequest,
|
'',
|
||||||
bondId: this.registryConfig.bondId
|
this.registryConfig.fee
|
||||||
},
|
|
||||||
this.registryConfig.privateKey,
|
|
||||||
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);
|
log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
applicationDeploymentRemovalRequestId: result.id,
|
applicationDeploymentRemovalRequestId: result.data.id,
|
||||||
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
|
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletedAuctionIds(auctionIds: string[]): Promise<string[]> {
|
getCrn (appName: string): string {
|
||||||
if (auctionIds.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const auctions = await this.registry.getAuctionsByIds(auctionIds);
|
|
||||||
|
|
||||||
const completedAuctions = auctions
|
|
||||||
.filter((auction: { id: string, status: string }) => auction.status === 'completed')
|
|
||||||
.map((auction: { id: string, status: string }) => auction.id);
|
|
||||||
|
|
||||||
return completedAuctions;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRecordsByName(name: string): Promise<any> {
|
|
||||||
return this.registry.resolveNames([name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAuctionData(auctionId: string): Promise<any> {
|
|
||||||
return this.registry.getAuctionsByIds([auctionId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendTokensToAccount(receiverAddress: string, amount: string): Promise<DeliverTxResponse> {
|
|
||||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
|
||||||
const account = await this.getAccount();
|
|
||||||
const laconicClient = await this.registry.getLaconicClient(account);
|
|
||||||
const txResponse: DeliverTxResponse =
|
|
||||||
await registryTransactionWithRetry(() =>
|
|
||||||
laconicClient.sendTokens(account.address, receiverAddress,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
denom: 'alnt',
|
|
||||||
amount
|
|
||||||
}
|
|
||||||
],
|
|
||||||
fee || DEFAULT_GAS_ESTIMATION_MULTIPLIER)
|
|
||||||
);
|
|
||||||
|
|
||||||
return txResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAccount(): Promise<Account> {
|
|
||||||
const account = new Account(Buffer.from(this.registryConfig.privateKey, 'hex'));
|
|
||||||
await account.init();
|
|
||||||
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTxResponse(txHash: string): Promise<IndexedTx | null> {
|
|
||||||
const account = await this.getAccount();
|
|
||||||
const laconicClient = await this.registry.getLaconicClient(account);
|
|
||||||
const txResponse: IndexedTx | null = await laconicClient.getTx(txHash);
|
|
||||||
|
|
||||||
return txResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
getLrn(appName: string): string {
|
|
||||||
assert(this.registryConfig.authority, "Authority doesn't exist");
|
assert(this.registryConfig.authority, "Authority doesn't exist");
|
||||||
return `lrn://${this.registryConfig.authority}/applications/${appName}`;
|
return `crn://${this.registryConfig.authority}/applications/${appName}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import { Permission } from './entity/ProjectMember';
|
|||||||
import { Domain } from './entity/Domain';
|
import { Domain } from './entity/Domain';
|
||||||
import { Project } from './entity/Project';
|
import { Project } from './entity/Project';
|
||||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||||
import { AddProjectFromTemplateInput, AuctionParams, EnvironmentVariables } from './types';
|
import { AddProjectFromTemplateInput } from './types';
|
||||||
|
|
||||||
const log = debug('snowball:resolver');
|
const log = debug('snowball:resolver');
|
||||||
|
|
||||||
@ -22,8 +22,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
return service.getOrganizationsByUserId(context.user);
|
return service.getOrganizationsByUserId(context.user);
|
||||||
},
|
},
|
||||||
|
|
||||||
project: async (_: any, { projectId }: { projectId: string }, context: any) => {
|
project: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
return service.getProjectById(context.user, projectId);
|
return service.getProjectById(projectId);
|
||||||
},
|
},
|
||||||
|
|
||||||
projectsInOrganization: async (
|
projectsInOrganization: async (
|
||||||
@ -69,32 +69,6 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
) => {
|
) => {
|
||||||
return service.getDomainsByProjectId(projectId, filter);
|
return service.getDomainsByProjectId(projectId, filter);
|
||||||
},
|
},
|
||||||
|
|
||||||
getAuctionData: async (
|
|
||||||
_: any,
|
|
||||||
{ auctionId }: { auctionId: string },
|
|
||||||
) => {
|
|
||||||
return service.getAuctionData(auctionId);
|
|
||||||
},
|
|
||||||
|
|
||||||
deployers: async (_: any, __: any, context: any) => {
|
|
||||||
return service.getDeployers();
|
|
||||||
},
|
|
||||||
|
|
||||||
address: async (_: any, __: any, context: any) => {
|
|
||||||
return service.getAddress();
|
|
||||||
},
|
|
||||||
|
|
||||||
verifyTx: async (
|
|
||||||
_: any,
|
|
||||||
{
|
|
||||||
txHash,
|
|
||||||
amount,
|
|
||||||
senderAddress,
|
|
||||||
}: { txHash: string; amount: string; senderAddress: string },
|
|
||||||
) => {
|
|
||||||
return service.verifyTx(txHash, amount, senderAddress);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Return error in GQL response
|
// TODO: Return error in GQL response
|
||||||
@ -229,16 +203,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
{
|
{
|
||||||
organizationSlug,
|
organizationSlug,
|
||||||
data,
|
data,
|
||||||
lrn,
|
}: { organizationSlug: string; data: AddProjectFromTemplateInput },
|
||||||
auctionParams,
|
|
||||||
environmentVariables
|
|
||||||
}: {
|
|
||||||
organizationSlug: string;
|
|
||||||
data: AddProjectFromTemplateInput;
|
|
||||||
lrn: string;
|
|
||||||
auctionParams: AuctionParams;
|
|
||||||
environmentVariables: EnvironmentVariables[];
|
|
||||||
},
|
|
||||||
context: any,
|
context: any,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
@ -246,9 +211,6 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
context.user,
|
context.user,
|
||||||
organizationSlug,
|
organizationSlug,
|
||||||
data,
|
data,
|
||||||
lrn,
|
|
||||||
auctionParams,
|
|
||||||
environmentVariables
|
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
@ -261,27 +223,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
{
|
{
|
||||||
organizationSlug,
|
organizationSlug,
|
||||||
data,
|
data,
|
||||||
lrn,
|
}: { organizationSlug: string; data: DeepPartial<Project> },
|
||||||
auctionParams,
|
|
||||||
environmentVariables
|
|
||||||
}: {
|
|
||||||
organizationSlug: string;
|
|
||||||
data: DeepPartial<Project>;
|
|
||||||
lrn: string;
|
|
||||||
auctionParams: AuctionParams,
|
|
||||||
environmentVariables: EnvironmentVariables[];
|
|
||||||
},
|
|
||||||
context: any,
|
context: any,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await service.addProject(
|
return await service.addProject(context.user, organizationSlug, data);
|
||||||
context.user,
|
|
||||||
organizationSlug,
|
|
||||||
data,
|
|
||||||
lrn,
|
|
||||||
auctionParams,
|
|
||||||
environmentVariables
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -5,6 +5,19 @@ import { authenticateUser, createUser } from '../turnkey-backend';
|
|||||||
|
|
||||||
const router = Router();
|
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
|
// Turnkey
|
||||||
//
|
//
|
||||||
@ -27,7 +40,7 @@ router.post('/register', async (req, res) => {
|
|||||||
userEmail: email,
|
userEmail: email,
|
||||||
userName: email.split('@')[0],
|
userName: email.split('@')[0],
|
||||||
});
|
});
|
||||||
req.session.address = user.id;
|
req.session.userId = user.id;
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -39,7 +52,7 @@ router.post('/authenticate', async (req, res) => {
|
|||||||
signedWhoamiRequest,
|
signedWhoamiRequest,
|
||||||
);
|
);
|
||||||
if (user) {
|
if (user) {
|
||||||
req.session.address = user.id;
|
req.session.userId = user.id;
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
} else {
|
} else {
|
||||||
res.sendStatus(401);
|
res.sendStatus(401);
|
||||||
@ -47,10 +60,11 @@ router.post('/authenticate', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//
|
//
|
||||||
// SIWE Auth
|
// Lit
|
||||||
//
|
//
|
||||||
|
|
||||||
router.post('/validate', async (req, res) => {
|
router.post('/validate', async (req, res) => {
|
||||||
const { message, signature } = req.body;
|
const { message, signature, action } = req.body;
|
||||||
const { success, data } = await new SiweMessage(message).verify({
|
const { success, data } = await new SiweMessage(message).verify({
|
||||||
signature,
|
signature,
|
||||||
});
|
});
|
||||||
@ -61,20 +75,23 @@ router.post('/validate', async (req, res) => {
|
|||||||
const service: Service = req.app.get('service');
|
const service: Service = req.app.get('service');
|
||||||
const user = await service.getUserByEthAddress(data.address);
|
const user = await service.getUserByEthAddress(data.address);
|
||||||
|
|
||||||
if (!user) {
|
if (action === 'signup') {
|
||||||
|
if (user) {
|
||||||
|
return res.send({ success: false, error: 'user_already_exists' });
|
||||||
|
}
|
||||||
const newUser = await service.createUser({
|
const newUser = await service.createUser({
|
||||||
ethAddress: data.address,
|
ethAddress: data.address,
|
||||||
email: `${data.address}@example.com`,
|
email: '',
|
||||||
|
name: '',
|
||||||
subOrgId: '',
|
subOrgId: '',
|
||||||
turnkeyWalletId: '',
|
turnkeyWalletId: '',
|
||||||
});
|
});
|
||||||
|
req.session.userId = newUser.id;
|
||||||
// SIWESession from the web3modal library requires both address and chain ID
|
} else if (action === 'login') {
|
||||||
req.session.address = newUser.id;
|
if (!user) {
|
||||||
req.session.chainId = data.chainId;
|
return res.send({ success: false, error: 'user_not_found' });
|
||||||
} else {
|
}
|
||||||
req.session.address = user.id;
|
req.session.userId = user.id;
|
||||||
req.session.chainId = data.chainId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send({ success });
|
res.send({ success });
|
||||||
@ -84,10 +101,9 @@ router.post('/validate', async (req, res) => {
|
|||||||
// General
|
// General
|
||||||
//
|
//
|
||||||
router.get('/session', (req, res) => {
|
router.get('/session', (req, res) => {
|
||||||
if (req.session.address && req.session.chainId) {
|
if (req.session.userId) {
|
||||||
res.send({
|
res.send({
|
||||||
address: req.session.address,
|
userId: req.session.userId,
|
||||||
chainId: req.session.chainId
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(401).send({ error: 'Unauthorized: No active session' });
|
res.status(401).send({ error: 'Unauthorized: No active session' });
|
||||||
@ -95,12 +111,9 @@ router.get('/session', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post('/logout', (req, res) => {
|
router.post('/logout', (req, res) => {
|
||||||
req.session.destroy((err) => {
|
// This is how you clear cookie-session
|
||||||
if (err) {
|
(req as any).session = null;
|
||||||
return res.send({ success: false });
|
res.send({ success: true });
|
||||||
}
|
|
||||||
res.send({ success: true });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
@ -3,7 +3,7 @@ import { Router } from 'express';
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/version', async (req, res) => {
|
router.get('/version', async (req, res) => {
|
||||||
return res.send({ version: '0.0.9' });
|
return res.send({ version: '0.0.8' });
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
@ -22,13 +22,6 @@ enum DeploymentStatus {
|
|||||||
Deleting
|
Deleting
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AuctionStatus {
|
|
||||||
completed
|
|
||||||
reveal
|
|
||||||
commit
|
|
||||||
expired
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DomainStatus {
|
enum DomainStatus {
|
||||||
Live
|
Live
|
||||||
Pending
|
Pending
|
||||||
@ -72,13 +65,8 @@ type Project {
|
|||||||
repository: String!
|
repository: String!
|
||||||
prodBranch: String!
|
prodBranch: String!
|
||||||
description: String
|
description: String
|
||||||
deployers: [Deployer!]
|
|
||||||
auctionId: String
|
|
||||||
fundsReleased: Boolean
|
|
||||||
template: String
|
template: String
|
||||||
framework: String
|
framework: String
|
||||||
paymentAddress: String!
|
|
||||||
txHash: String!
|
|
||||||
webhooks: [String!]
|
webhooks: [String!]
|
||||||
members: [ProjectMember!]
|
members: [ProjectMember!]
|
||||||
environmentVariables: [EnvironmentVariable!]
|
environmentVariables: [EnvironmentVariable!]
|
||||||
@ -86,7 +74,7 @@ type Project {
|
|||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
organization: Organization!
|
organization: Organization!
|
||||||
icon: String
|
icon: String
|
||||||
baseDomains: [String!]
|
subDomain: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectMember {
|
type ProjectMember {
|
||||||
@ -106,10 +94,7 @@ type Deployment {
|
|||||||
commitMessage: String!
|
commitMessage: String!
|
||||||
url: String
|
url: String
|
||||||
environment: Environment!
|
environment: Environment!
|
||||||
deployer: Deployer
|
|
||||||
applicationDeploymentRequestId: String
|
|
||||||
isCurrent: Boolean!
|
isCurrent: Boolean!
|
||||||
baseDomain: String
|
|
||||||
status: DeploymentStatus!
|
status: DeploymentStatus!
|
||||||
createdAt: String!
|
createdAt: String!
|
||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
@ -135,17 +120,6 @@ type EnvironmentVariable {
|
|||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Deployer {
|
|
||||||
deployerLrn: String!
|
|
||||||
deployerId: String!
|
|
||||||
deployerApiUrl: String!
|
|
||||||
minimumPayment: String
|
|
||||||
paymentAddress: String
|
|
||||||
createdAt: String!
|
|
||||||
updatedAt: String!
|
|
||||||
baseDomain: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthResult {
|
type AuthResult {
|
||||||
token: String!
|
token: String!
|
||||||
}
|
}
|
||||||
@ -162,8 +136,6 @@ input AddProjectFromTemplateInput {
|
|||||||
owner: String!
|
owner: String!
|
||||||
name: String!
|
name: String!
|
||||||
isPrivate: Boolean!
|
isPrivate: Boolean!
|
||||||
paymentAddress: String!
|
|
||||||
txHash: String!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input AddProjectInput {
|
input AddProjectInput {
|
||||||
@ -171,8 +143,6 @@ input AddProjectInput {
|
|||||||
repository: String!
|
repository: String!
|
||||||
prodBranch: String!
|
prodBranch: String!
|
||||||
template: String
|
template: String
|
||||||
paymentAddress: String!
|
|
||||||
txHash: String!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input UpdateProjectInput {
|
input UpdateProjectInput {
|
||||||
@ -212,48 +182,6 @@ input FilterDomainsInput {
|
|||||||
status: DomainStatus
|
status: DomainStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
type Fee {
|
|
||||||
type: String!
|
|
||||||
quantity: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Bid {
|
|
||||||
auctionId: String!
|
|
||||||
bidderAddress: String!
|
|
||||||
status: String!
|
|
||||||
commitHash: String!
|
|
||||||
commitTime: String
|
|
||||||
commitFee: Fee
|
|
||||||
revealTime: String
|
|
||||||
revealFee: Fee
|
|
||||||
bidAmount: Fee
|
|
||||||
}
|
|
||||||
|
|
||||||
type Auction {
|
|
||||||
id: String!
|
|
||||||
kind: String!
|
|
||||||
status: String!
|
|
||||||
ownerAddress: String!
|
|
||||||
createTime: String!
|
|
||||||
commitsEndTime: String!
|
|
||||||
revealsEndTime: String!
|
|
||||||
commitFee: Fee!
|
|
||||||
revealFee: Fee!
|
|
||||||
minimumBid: Fee
|
|
||||||
winnerAddresses: [String!]!
|
|
||||||
winnerBids: [Fee!]
|
|
||||||
winnerPrice: Fee
|
|
||||||
maxPrice: Fee
|
|
||||||
numProviders: Int!
|
|
||||||
fundsReleased: Boolean!
|
|
||||||
bids: [Bid!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
input AuctionParams {
|
|
||||||
maxPrice: String,
|
|
||||||
numProviders: Int,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
user: User!
|
user: User!
|
||||||
organizations: [Organization!]
|
organizations: [Organization!]
|
||||||
@ -264,11 +192,7 @@ type Query {
|
|||||||
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
||||||
projectMembers(projectId: String!): [ProjectMember!]
|
projectMembers(projectId: String!): [ProjectMember!]
|
||||||
searchProjects(searchText: String!): [Project!]
|
searchProjects(searchText: String!): [Project!]
|
||||||
getAuctionData(auctionId: String!): Auction!
|
|
||||||
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
||||||
deployers: [Deployer]
|
|
||||||
address: String!
|
|
||||||
verifyTx(txHash: String!, amount: String!, senderAddress: String!): Boolean!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
@ -291,17 +215,8 @@ type Mutation {
|
|||||||
addProjectFromTemplate(
|
addProjectFromTemplate(
|
||||||
organizationSlug: String!
|
organizationSlug: String!
|
||||||
data: AddProjectFromTemplateInput
|
data: AddProjectFromTemplateInput
|
||||||
lrn: String
|
|
||||||
auctionParams: AuctionParams
|
|
||||||
environmentVariables: [AddEnvironmentVariableInput!]
|
|
||||||
): Project!
|
|
||||||
addProject(
|
|
||||||
organizationSlug: String!
|
|
||||||
data: AddProjectInput!
|
|
||||||
lrn: String
|
|
||||||
auctionParams: AuctionParams
|
|
||||||
environmentVariables: [AddEnvironmentVariableInput!]
|
|
||||||
): Project!
|
): Project!
|
||||||
|
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
||||||
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
|
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
|
||||||
redeployToProd(deploymentId: String!): Boolean!
|
redeployToProd(deploymentId: String!): Boolean!
|
||||||
deleteProject(projectId: String!): Boolean!
|
deleteProject(projectId: String!): Boolean!
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
ApolloServerPluginLandingPageLocalDefault,
|
ApolloServerPluginLandingPageLocalDefault,
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
} from 'apollo-server-core';
|
} from 'apollo-server-core';
|
||||||
import session from 'express-session';
|
import cookieSession from 'cookie-session';
|
||||||
|
|
||||||
import { TypeSource } from '@graphql-tools/utils';
|
import { TypeSource } from '@graphql-tools/utils';
|
||||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||||
@ -22,13 +22,9 @@ import { Service } from './service';
|
|||||||
|
|
||||||
const log = debug('snowball:server');
|
const log = debug('snowball:server');
|
||||||
|
|
||||||
// Set cookie expiration to 1 month in milliseconds
|
|
||||||
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
declare module 'express-session' {
|
declare module 'express-session' {
|
||||||
interface SessionData {
|
interface SessionData {
|
||||||
address: string;
|
userId: string;
|
||||||
chainId: number;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,13 +54,14 @@ export const createAndStartServer = async (
|
|||||||
context: async ({ req }) => {
|
context: async ({ req }) => {
|
||||||
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
||||||
|
|
||||||
const { address } = req.session;
|
const { userId } = req.session;
|
||||||
|
|
||||||
if (!address) {
|
if (!userId) {
|
||||||
throw new AuthenticationError('Unauthorized: No active session');
|
throw new AuthenticationError('Unauthorized: No active session');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await service.getUser(address);
|
const user = await service.getUser(userId);
|
||||||
|
|
||||||
return { user };
|
return { user };
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
@ -83,25 +80,20 @@ export const createAndStartServer = async (
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const sessionOptions: session.SessionOptions = {
|
|
||||||
secret: secret,
|
|
||||||
resave: false,
|
|
||||||
saveUninitialized: true,
|
|
||||||
cookie: {
|
|
||||||
secure: new URL(appOriginUrl).protocol === 'https:',
|
|
||||||
maxAge: COOKIE_MAX_AGE,
|
|
||||||
domain: domain || undefined,
|
|
||||||
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (trustProxy) {
|
if (trustProxy) {
|
||||||
// trust first proxy
|
// trust first proxy
|
||||||
app.set('trust proxy', 1);
|
app.set('trust proxy', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
session(sessionOptions)
|
cookieSession({
|
||||||
|
secret: secret,
|
||||||
|
secure: new URL(appOriginUrl).protocol === 'https:',
|
||||||
|
// 23 hours (less than 24 hours to avoid sessionSigs expiration issues)
|
||||||
|
maxAge: 23 * 60 * 60 * 1000,
|
||||||
|
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
|
||||||
|
domain: domain || undefined,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.applyMiddleware({
|
server.applyMiddleware({
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { DeepPartial, FindOptionsWhere, IsNull, Not } from 'typeorm';
|
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
||||||
import { Octokit, RequestError } from 'octokit';
|
import { Octokit, RequestError } from 'octokit';
|
||||||
|
|
||||||
import { OAuthApp } from '@octokit/oauth-app';
|
import { OAuthApp } from '@octokit/oauth-app';
|
||||||
|
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
import { ApplicationRecord, Deployment, DeploymentStatus, Environment } from './entity/Deployment';
|
import { Deployment, DeploymentStatus, Environment } from './entity/Deployment';
|
||||||
import { Domain } from './entity/Domain';
|
import { Domain } from './entity/Domain';
|
||||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||||
import { Organization } from './entity/Organization';
|
import { Organization } from './entity/Organization';
|
||||||
@ -14,19 +14,15 @@ import { Project } from './entity/Project';
|
|||||||
import { Permission, ProjectMember } from './entity/ProjectMember';
|
import { Permission, ProjectMember } from './entity/ProjectMember';
|
||||||
import { User } from './entity/User';
|
import { User } from './entity/User';
|
||||||
import { Registry } from './registry';
|
import { Registry } from './registry';
|
||||||
import { Deployer } from './entity/Deployer';
|
|
||||||
import { GitHubConfig, RegistryConfig } from './config';
|
import { GitHubConfig, RegistryConfig } from './config';
|
||||||
import {
|
import {
|
||||||
AddProjectFromTemplateInput,
|
AddProjectFromTemplateInput,
|
||||||
AppDeploymentRecord,
|
AppDeploymentRecord,
|
||||||
AppDeploymentRemovalRecord,
|
AppDeploymentRemovalRecord,
|
||||||
AuctionParams,
|
|
||||||
DeployerRecord,
|
|
||||||
EnvironmentVariables,
|
|
||||||
GitPushEventPayload,
|
GitPushEventPayload,
|
||||||
|
PackageJSON,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { Role } from './entity/UserOrganization';
|
import { Role } from './entity/UserOrganization';
|
||||||
import { getRepoDetails } from './utils';
|
|
||||||
|
|
||||||
const log = debug('snowball:service');
|
const log = debug('snowball:service');
|
||||||
|
|
||||||
@ -43,16 +39,15 @@ interface Config {
|
|||||||
export class Service {
|
export class Service {
|
||||||
private db: Database;
|
private db: Database;
|
||||||
private oauthApp: OAuthApp;
|
private oauthApp: OAuthApp;
|
||||||
private laconicRegistry: Registry;
|
private registry: Registry;
|
||||||
private config: Config;
|
private config: Config;
|
||||||
|
|
||||||
private deployRecordCheckTimeout?: NodeJS.Timeout;
|
private deployRecordCheckTimeout?: NodeJS.Timeout;
|
||||||
private auctionStatusCheckTimeout?: NodeJS.Timeout;
|
|
||||||
|
|
||||||
constructor(config: Config, db: Database, app: OAuthApp, registry: Registry) {
|
constructor(config: Config, db: Database, app: OAuthApp, registry: Registry) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.oauthApp = app;
|
this.oauthApp = app;
|
||||||
this.laconicRegistry = registry;
|
this.registry = registry;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
@ -65,8 +60,6 @@ export class Service {
|
|||||||
this.checkDeployRecordsAndUpdate();
|
this.checkDeployRecordsAndUpdate();
|
||||||
// Start check for ApplicationDeploymentRemovalRecords asynchronously
|
// Start check for ApplicationDeploymentRemovalRecords asynchronously
|
||||||
this.checkDeploymentRemovalRecordsAndUpdate();
|
this.checkDeploymentRemovalRecordsAndUpdate();
|
||||||
// Start check for Deployment Auctions asynchronously
|
|
||||||
this.checkAuctionStatus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,7 +67,6 @@ export class Service {
|
|||||||
*/
|
*/
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
clearTimeout(this.deployRecordCheckTimeout);
|
clearTimeout(this.deployRecordCheckTimeout);
|
||||||
clearTimeout(this.auctionStatusCheckTimeout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -116,7 +108,7 @@ export class Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch ApplicationDeploymentRecord for deployments
|
// Fetch ApplicationDeploymentRecord for deployments
|
||||||
const records = await this.laconicRegistry.getDeploymentRecords(deployments);
|
const records = await this.registry.getDeploymentRecords(deployments);
|
||||||
log(`Found ${records.length} ApplicationDeploymentRecords`);
|
log(`Found ${records.length} ApplicationDeploymentRecords`);
|
||||||
|
|
||||||
// Update deployments for which ApplicationDeploymentRecords were returned
|
// Update deployments for which ApplicationDeploymentRecords were returned
|
||||||
@ -149,7 +141,7 @@ export class Service {
|
|||||||
|
|
||||||
// Fetch ApplicationDeploymentRemovalRecords for deployments
|
// Fetch ApplicationDeploymentRemovalRecords for deployments
|
||||||
const records =
|
const records =
|
||||||
await this.laconicRegistry.getDeploymentRemovalRecords(deployments);
|
await this.registry.getDeploymentRemovalRecords(deployments);
|
||||||
log(`Found ${records.length} ApplicationDeploymentRemovalRecords`);
|
log(`Found ${records.length} ApplicationDeploymentRemovalRecords`);
|
||||||
|
|
||||||
// Update deployments for which ApplicationDeploymentRemovalRecords were returned
|
// Update deployments for which ApplicationDeploymentRemovalRecords were returned
|
||||||
@ -165,28 +157,41 @@ export class Service {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update deployments with ApplicationDeploymentRecord data
|
* Update deployments with ApplicationDeploymentRecord data
|
||||||
* Deployments that are completed but not updated in DB
|
|
||||||
*/
|
*/
|
||||||
async updateDeploymentsWithRecordData(
|
async updateDeploymentsWithRecordData(
|
||||||
records: AppDeploymentRecord[],
|
records: AppDeploymentRecord[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Fetch the deployments to be updated using deployment requestId
|
// Get deployments for ApplicationDeploymentRecords
|
||||||
const deployments = await this.db.getDeployments({
|
const deployments = await this.db.getDeployments({
|
||||||
where: records.map((record) => ({
|
where: records.map((record) => ({
|
||||||
applicationDeploymentRequestId: record.attributes.request,
|
applicationRecordId: record.attributes.application,
|
||||||
})),
|
})),
|
||||||
relations: {
|
|
||||||
deployer: true,
|
|
||||||
project: true,
|
|
||||||
},
|
|
||||||
order: {
|
order: {
|
||||||
createdAt: 'DESC',
|
createdAt: 'DESC',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get project IDs of deployments that are in production environment
|
||||||
|
const productionDeploymentProjectIds = deployments.reduce(
|
||||||
|
(acc, deployment): Set<string> => {
|
||||||
|
if (deployment.environment === Environment.Production) {
|
||||||
|
acc.add(deployment.projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
new Set<string>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set old deployments isCurrent to false
|
||||||
|
await this.db.updateDeploymentsByProjectIds(
|
||||||
|
Array.from(productionDeploymentProjectIds),
|
||||||
|
{ isCurrent: false },
|
||||||
|
);
|
||||||
|
|
||||||
const recordToDeploymentsMap = deployments.reduce(
|
const recordToDeploymentsMap = deployments.reduce(
|
||||||
(acc: { [key: string]: Deployment }, deployment) => {
|
(acc: { [key: string]: Deployment }, deployment) => {
|
||||||
acc[deployment.applicationDeploymentRequestId!] = deployment;
|
acc[deployment.applicationRecordId] = deployment;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
@ -194,57 +199,22 @@ export class Service {
|
|||||||
|
|
||||||
// Update deployment data for ApplicationDeploymentRecords
|
// Update deployment data for ApplicationDeploymentRecords
|
||||||
const deploymentUpdatePromises = records.map(async (record) => {
|
const deploymentUpdatePromises = records.map(async (record) => {
|
||||||
const deployment = recordToDeploymentsMap[record.attributes.request];
|
const deployment = recordToDeploymentsMap[record.attributes.application];
|
||||||
|
|
||||||
if (!deployment.project) {
|
await this.db.updateDeploymentById(deployment.id, {
|
||||||
log(`Project ${deployment.projectId} not found`);
|
applicationDeploymentRecordId: record.id,
|
||||||
return;
|
applicationDeploymentRecordData: record.attributes,
|
||||||
} else {
|
url: record.attributes.url,
|
||||||
deployment.applicationDeploymentRecordId = record.id;
|
status: DeploymentStatus.Ready,
|
||||||
deployment.applicationDeploymentRecordData = record.attributes;
|
isCurrent: deployment.environment === Environment.Production,
|
||||||
deployment.url = record.attributes.url;
|
});
|
||||||
deployment.status = DeploymentStatus.Ready;
|
|
||||||
deployment.isCurrent = deployment.environment === Environment.Production;
|
|
||||||
|
|
||||||
await this.db.updateDeploymentById(deployment.id, deployment);
|
log(
|
||||||
|
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`,
|
||||||
// Release deployer funds on successful deployment
|
);
|
||||||
if (!deployment.project.fundsReleased) {
|
|
||||||
const fundsReleased = await this.releaseDeployerFundsByProjectId(deployment.projectId);
|
|
||||||
|
|
||||||
// Return remaining amount to owner
|
|
||||||
await this.returnUserFundsByProjectId(deployment.projectId, true);
|
|
||||||
|
|
||||||
await this.db.updateProjectById(deployment.projectId, {
|
|
||||||
fundsReleased,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
log(
|
|
||||||
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(deploymentUpdatePromises);
|
await Promise.all(deploymentUpdatePromises);
|
||||||
|
|
||||||
// Get deployments that are in production environment
|
|
||||||
const prodDeployments = Object.values(recordToDeploymentsMap).filter(deployment => deployment.isCurrent);
|
|
||||||
|
|
||||||
// Set the isCurrent state to false for the old deployments
|
|
||||||
for (const deployment of prodDeployments) {
|
|
||||||
const projectDeployments = await this.db.getDeploymentsByProjectId(deployment.projectId);
|
|
||||||
const oldDeployments = projectDeployments
|
|
||||||
.filter(projectDeployment => projectDeployment.deployer.deployerLrn === deployment.deployer.deployerLrn && projectDeployment.id !== deployment.id);
|
|
||||||
for (const oldDeployment of oldDeployments) {
|
|
||||||
await this.db.updateDeployment(
|
|
||||||
{ id: oldDeployment.id },
|
|
||||||
{ isCurrent: false }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(deploymentUpdatePromises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -292,45 +262,6 @@ export class Service {
|
|||||||
await Promise.all(deploymentUpdatePromises);
|
await Promise.all(deploymentUpdatePromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks the status for all ongoing auctions
|
|
||||||
* Calls the createDeploymentFromAuction method for deployments with completed auctions
|
|
||||||
*/
|
|
||||||
async checkAuctionStatus(): Promise<void> {
|
|
||||||
const projects = await this.db.allProjectsWithoutDeployments();
|
|
||||||
|
|
||||||
const validAuctionIds = projects.map((project) => project.auctionId)
|
|
||||||
.filter((id): id is string => Boolean(id));
|
|
||||||
const completedAuctionIds = await this.laconicRegistry.getCompletedAuctionIds(validAuctionIds);
|
|
||||||
|
|
||||||
const projectsToBedeployed = projects.filter((project) =>
|
|
||||||
completedAuctionIds.includes(project.auctionId!)
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const project of projectsToBedeployed) {
|
|
||||||
const deployerRecords = await this.laconicRegistry.getAuctionWinningDeployerRecords(project!.auctionId!);
|
|
||||||
|
|
||||||
if (!deployerRecords) {
|
|
||||||
log(`No winning deployer for auction ${project!.auctionId}`);
|
|
||||||
|
|
||||||
// Return all funds to the owner
|
|
||||||
await this.returnUserFundsByProjectId(project.id, false)
|
|
||||||
} else {
|
|
||||||
const deployers = await this.saveDeployersByDeployerRecords(deployerRecords);
|
|
||||||
for (const deployer of deployers) {
|
|
||||||
log(`Creating deployment for deployer ${deployer.deployerLrn}`);
|
|
||||||
await this.createDeploymentFromAuction(project, deployer);
|
|
||||||
// Update project with deployer
|
|
||||||
await this.updateProjectWithDeployer(project.id, deployer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.auctionStatusCheckTimeout = setTimeout(() => {
|
|
||||||
this.checkAuctionStatus();
|
|
||||||
}, this.config.registryConfig.checkAuctionStatusDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUser(userId: string): Promise<User | null> {
|
async getUser(userId: string): Promise<User | null> {
|
||||||
return this.db.getUser({
|
return this.db.getUser({
|
||||||
where: {
|
where: {
|
||||||
@ -364,7 +295,7 @@ export class Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createUser(params: {
|
async createUser(params: {
|
||||||
name?: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
subOrgId: string;
|
subOrgId: string;
|
||||||
ethAddress: string;
|
ethAddress: string;
|
||||||
@ -407,13 +338,8 @@ export class Service {
|
|||||||
return dbOrganizations;
|
return dbOrganizations;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectById(user: User, projectId: string): Promise<Project | null> {
|
async getProjectById(projectId: string): Promise<Project | null> {
|
||||||
const dbProject = await this.db.getProjectById(projectId);
|
const dbProject = await this.db.getProjectById(projectId);
|
||||||
|
|
||||||
if (dbProject && dbProject.owner.id !== user.id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbProject;
|
return dbProject;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,7 +519,6 @@ export class Service {
|
|||||||
domain: prodBranchDomains[0],
|
domain: prodBranchDomains[0],
|
||||||
commitHash: oldDeployment.commitHash,
|
commitHash: oldDeployment.commitHash,
|
||||||
commitMessage: oldDeployment.commitMessage,
|
commitMessage: oldDeployment.commitMessage,
|
||||||
deployer: oldDeployment.deployer
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return newDeployment;
|
return newDeployment;
|
||||||
@ -603,20 +528,44 @@ export class Service {
|
|||||||
userId: string,
|
userId: string,
|
||||||
octokit: Octokit,
|
octokit: Octokit,
|
||||||
data: DeepPartial<Deployment>,
|
data: DeepPartial<Deployment>,
|
||||||
deployerLrn?: string
|
|
||||||
): Promise<Deployment> {
|
): Promise<Deployment> {
|
||||||
assert(data.project?.repository, 'Project repository not found');
|
assert(data.project?.repository, 'Project repository not found');
|
||||||
log(
|
log(
|
||||||
`Creating deployment in project ${data.project.name} from branch ${data.branch}`,
|
`Creating deployment in project ${data.project.name} from branch ${data.branch}`,
|
||||||
);
|
);
|
||||||
|
const [owner, repo] = data.project.repository.split('/');
|
||||||
|
|
||||||
|
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
path: 'package.json',
|
||||||
|
ref: data.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;
|
||||||
|
|
||||||
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
// TODO: Set environment variables for each deployment (environment variables can`t be set in application record)
|
||||||
const { applicationRecordId, applicationRecordData } =
|
const { applicationRecordId, applicationRecordData } =
|
||||||
await this.laconicRegistry.createApplicationRecord({
|
await this.registry.createApplicationRecord({
|
||||||
octokit,
|
appName: repo,
|
||||||
repository: data.project.repository,
|
packageJSON,
|
||||||
appType: data.project!.template!,
|
appType: data.project!.template!,
|
||||||
commitHash: data.commitHash!,
|
commitHash: data.commitHash!,
|
||||||
|
repoUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update previous deployment with prod branch domain
|
// Update previous deployment with prod branch domain
|
||||||
@ -632,138 +581,6 @@ export class Service {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let deployer;
|
|
||||||
if (deployerLrn) {
|
|
||||||
deployer = await this.db.getDeployerByLRN(deployerLrn);
|
|
||||||
} else {
|
|
||||||
deployer = data.deployer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newDeployment = await this.createDeploymentFromData(userId, data, deployer!.deployerLrn!, applicationRecordId, applicationRecordData);
|
|
||||||
|
|
||||||
const { repo, repoUrl } = await getRepoDetails(octokit, data.project.repository, data.commitHash);
|
|
||||||
const environmentVariablesObj = await this.getEnvVariables(data.project!.id!);
|
|
||||||
// To set project DNS
|
|
||||||
if (data.environment === Environment.Production) {
|
|
||||||
// On deleting deployment later, project DNS deployment is also deleted
|
|
||||||
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
|
|
||||||
await this.laconicRegistry.createApplicationDeploymentRequest({
|
|
||||||
deployment: newDeployment,
|
|
||||||
appName: repo,
|
|
||||||
repository: repoUrl,
|
|
||||||
environmentVariables: environmentVariablesObj,
|
|
||||||
dns: `${newDeployment.project.name}`,
|
|
||||||
lrn: deployer!.deployerLrn!,
|
|
||||||
payment: data.project.txHash,
|
|
||||||
auctionId: data.project.auctionId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
|
|
||||||
await this.laconicRegistry.createApplicationDeploymentRequest({
|
|
||||||
deployment: newDeployment,
|
|
||||||
appName: repo,
|
|
||||||
repository: repoUrl,
|
|
||||||
lrn: deployer!.deployerLrn!,
|
|
||||||
environmentVariables: environmentVariablesObj,
|
|
||||||
dns: `${newDeployment.project.name}-${newDeployment.id}`,
|
|
||||||
payment: data.project.txHash,
|
|
||||||
auctionId: data.project.auctionId
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.db.updateDeploymentById(newDeployment.id, {
|
|
||||||
applicationDeploymentRequestId,
|
|
||||||
applicationDeploymentRequestData,
|
|
||||||
});
|
|
||||||
|
|
||||||
return newDeployment;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createDeploymentFromAuction(
|
|
||||||
project: DeepPartial<Project>,
|
|
||||||
deployer: Deployer
|
|
||||||
): Promise<Deployment> {
|
|
||||||
const octokit = await this.getOctokit(project.ownerId!);
|
|
||||||
const [owner, repo] = project.repository!.split('/');
|
|
||||||
|
|
||||||
const repoUrl = (
|
|
||||||
await octokit.rest.repos.get({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
})
|
|
||||||
).data.html_url;
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: [latestCommit],
|
|
||||||
} = await octokit.rest.repos.listCommits({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
sha: project.prodBranch,
|
|
||||||
per_page: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const lrn = this.laconicRegistry.getLrn(repo);
|
|
||||||
const [record] = await this.laconicRegistry.getRecordsByName(lrn);
|
|
||||||
const applicationRecordId = record.id;
|
|
||||||
const applicationRecordData = record.attributes;
|
|
||||||
|
|
||||||
const deployerLrn = deployer!.deployerLrn
|
|
||||||
|
|
||||||
// Create deployment with prod branch and latest commit
|
|
||||||
const deploymentData = {
|
|
||||||
project,
|
|
||||||
branch: project.prodBranch,
|
|
||||||
environment: Environment.Production,
|
|
||||||
domain: null,
|
|
||||||
commitHash: latestCommit.sha,
|
|
||||||
commitMessage: latestCommit.commit.message,
|
|
||||||
};
|
|
||||||
|
|
||||||
const newDeployment = await this.createDeploymentFromData(project.ownerId!, deploymentData, deployerLrn, applicationRecordId, applicationRecordData);
|
|
||||||
|
|
||||||
const environmentVariablesObj = await this.getEnvVariables(project!.id!);
|
|
||||||
// To set project DNS
|
|
||||||
if (deploymentData.environment === Environment.Production) {
|
|
||||||
// On deleting deployment later, project DNS deployment is also deleted
|
|
||||||
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
|
|
||||||
await this.laconicRegistry.createApplicationDeploymentRequest({
|
|
||||||
deployment: newDeployment,
|
|
||||||
appName: repo,
|
|
||||||
repository: repoUrl,
|
|
||||||
environmentVariables: environmentVariablesObj,
|
|
||||||
dns: `${newDeployment.project.name}`,
|
|
||||||
auctionId: project.auctionId!,
|
|
||||||
lrn: deployerLrn,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
|
|
||||||
// Create requests for all the deployers
|
|
||||||
await this.laconicRegistry.createApplicationDeploymentRequest({
|
|
||||||
deployment: newDeployment,
|
|
||||||
appName: repo,
|
|
||||||
repository: repoUrl,
|
|
||||||
auctionId: project.auctionId!,
|
|
||||||
lrn: deployerLrn,
|
|
||||||
environmentVariables: environmentVariablesObj,
|
|
||||||
dns: `${newDeployment.project.name}-${newDeployment.id}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.db.updateDeploymentById(newDeployment.id, {
|
|
||||||
applicationDeploymentRequestId,
|
|
||||||
applicationDeploymentRequestData,
|
|
||||||
});
|
|
||||||
|
|
||||||
return newDeployment;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createDeploymentFromData(
|
|
||||||
userId: string,
|
|
||||||
data: DeepPartial<Deployment>,
|
|
||||||
deployerLrn: string,
|
|
||||||
applicationRecordId: string,
|
|
||||||
applicationRecordData: ApplicationRecord,
|
|
||||||
): Promise<Deployment> {
|
|
||||||
const newDeployment = await this.db.addDeployment({
|
const newDeployment = await this.db.addDeployment({
|
||||||
project: data.project,
|
project: data.project,
|
||||||
branch: data.branch,
|
branch: data.branch,
|
||||||
@ -777,43 +594,60 @@ export class Service {
|
|||||||
createdBy: Object.assign(new User(), {
|
createdBy: Object.assign(new User(), {
|
||||||
id: userId,
|
id: userId,
|
||||||
}),
|
}),
|
||||||
deployer: Object.assign(new Deployer(), {
|
|
||||||
deployerLrn,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
log(`Created deployment ${newDeployment.id}`);
|
log(
|
||||||
|
`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`,
|
||||||
|
);
|
||||||
|
|
||||||
return newDeployment;
|
const environmentVariables =
|
||||||
}
|
await this.db.getEnvironmentVariablesByProjectId(data.project.id!, {
|
||||||
|
environment: Environment.Production,
|
||||||
|
});
|
||||||
|
|
||||||
async updateProjectWithDeployer(
|
const environmentVariablesObj = environmentVariables.reduce(
|
||||||
projectId: string,
|
(acc, env) => {
|
||||||
deployer: Deployer
|
acc[env.key] = env.value;
|
||||||
): Promise<Deployer> {
|
|
||||||
const deploymentProject = await this.db.getProjects({
|
|
||||||
where: { id: projectId },
|
|
||||||
relations: ['deployers']
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!deploymentProject[0].deployers) {
|
return acc;
|
||||||
deploymentProject[0].deployers = [];
|
},
|
||||||
|
{} as { [key: string]: string },
|
||||||
|
);
|
||||||
|
|
||||||
|
// To set project DNS
|
||||||
|
if (data.environment === Environment.Production) {
|
||||||
|
// On deleting deployment later, project DNS deployment is also deleted
|
||||||
|
// So publish project DNS deployment first so that ApplicationDeploymentRecord for the same is available when deleting deployment later
|
||||||
|
await this.registry.createApplicationDeploymentRequest({
|
||||||
|
deployment: newDeployment,
|
||||||
|
appName: repo,
|
||||||
|
repository: repoUrl,
|
||||||
|
environmentVariables: environmentVariablesObj,
|
||||||
|
dns: `${newDeployment.project.name}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deploymentProject[0].deployers.push(deployer);
|
const { applicationDeploymentRequestId, applicationDeploymentRequestData } =
|
||||||
|
await this.registry.createApplicationDeploymentRequest({
|
||||||
|
deployment: newDeployment,
|
||||||
|
appName: repo,
|
||||||
|
repository: repoUrl,
|
||||||
|
environmentVariables: environmentVariablesObj,
|
||||||
|
dns: `${newDeployment.project.name}-${newDeployment.id}`,
|
||||||
|
});
|
||||||
|
|
||||||
await this.db.saveProject(deploymentProject[0]);
|
await this.db.updateDeploymentById(newDeployment.id, {
|
||||||
|
applicationDeploymentRequestId,
|
||||||
|
applicationDeploymentRequestData,
|
||||||
|
});
|
||||||
|
|
||||||
return deployer;
|
return newDeployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProjectFromTemplate(
|
async addProjectFromTemplate(
|
||||||
user: User,
|
user: User,
|
||||||
organizationSlug: string,
|
organizationSlug: string,
|
||||||
data: AddProjectFromTemplateInput,
|
data: AddProjectFromTemplateInput,
|
||||||
lrn?: string,
|
|
||||||
auctionParams?: AuctionParams,
|
|
||||||
environmentVariables?: EnvironmentVariables[],
|
|
||||||
): Promise<Project | undefined> {
|
): Promise<Project | undefined> {
|
||||||
try {
|
try {
|
||||||
const octokit = await this.getOctokit(user.id);
|
const octokit = await this.getOctokit(user.id);
|
||||||
@ -844,9 +678,7 @@ export class Service {
|
|||||||
repository: gitRepo.data.full_name,
|
repository: gitRepo.data.full_name,
|
||||||
// TODO: Set selected template
|
// TODO: Set selected template
|
||||||
template: 'webapp',
|
template: 'webapp',
|
||||||
paymentAddress: data.paymentAddress,
|
});
|
||||||
txHash: data.txHash
|
|
||||||
}, lrn, auctionParams, environmentVariables);
|
|
||||||
|
|
||||||
if (!project || !project.id) {
|
if (!project || !project.id) {
|
||||||
throw new Error('Failed to create project from template');
|
throw new Error('Failed to create project from template');
|
||||||
@ -863,26 +695,18 @@ export class Service {
|
|||||||
user: User,
|
user: User,
|
||||||
organizationSlug: string,
|
organizationSlug: string,
|
||||||
data: DeepPartial<Project>,
|
data: DeepPartial<Project>,
|
||||||
lrn?: string,
|
|
||||||
auctionParams?: AuctionParams,
|
|
||||||
environmentVariables?: EnvironmentVariables[],
|
|
||||||
): Promise<Project | undefined> {
|
): Promise<Project | undefined> {
|
||||||
const organization = await this.db.getOrganization({
|
const organization = await this.db.getOrganization({
|
||||||
where: {
|
where: {
|
||||||
slug: organizationSlug,
|
slug: organizationSlug,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new Error('Organization does not exist');
|
throw new Error('Organization does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = await this.db.addProject(user, organization.id, data);
|
const project = await this.db.addProject(user, organization.id, data);
|
||||||
|
|
||||||
if (environmentVariables) {
|
|
||||||
await this.addEnvironmentVariables(project.id, environmentVariables);
|
|
||||||
}
|
|
||||||
|
|
||||||
const octokit = await this.getOctokit(user.id);
|
const octokit = await this.getOctokit(user.id);
|
||||||
const [owner, repo] = project.repository.split('/');
|
const [owner, repo] = project.repository.split('/');
|
||||||
|
|
||||||
@ -895,59 +719,20 @@ export class Service {
|
|||||||
per_page: 1,
|
per_page: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (auctionParams) {
|
// Create deployment with prod branch and latest commit
|
||||||
// Create deployment with prod branch and latest commit
|
const deployment = await this.createDeployment(user.id, octokit, {
|
||||||
const deploymentData = {
|
project,
|
||||||
project,
|
branch: project.prodBranch,
|
||||||
branch: project.prodBranch,
|
environment: Environment.Production,
|
||||||
environment: Environment.Production,
|
domain: null,
|
||||||
domain: null,
|
commitHash: latestCommit.sha,
|
||||||
commitHash: latestCommit.sha,
|
commitMessage: latestCommit.commit.message,
|
||||||
commitMessage: latestCommit.commit.message,
|
});
|
||||||
};
|
|
||||||
const { applicationDeploymentAuctionId } = await this.laconicRegistry.createApplicationDeploymentAuction(repo, octokit, auctionParams!, deploymentData);
|
|
||||||
await this.updateProject(project.id, { auctionId: applicationDeploymentAuctionId });
|
|
||||||
} else {
|
|
||||||
const deployer = await this.db.getDeployerByLRN(lrn!);
|
|
||||||
|
|
||||||
if (!deployer) {
|
|
||||||
log('Invalid deployer LRN');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deployer.minimumPayment && project.txHash) {
|
|
||||||
const amountToBePaid = deployer?.minimumPayment.replace(/\D/g, '').toString();
|
|
||||||
|
|
||||||
const txResponse = await this.laconicRegistry.sendTokensToAccount(
|
|
||||||
deployer?.paymentAddress!,
|
|
||||||
amountToBePaid
|
|
||||||
);
|
|
||||||
|
|
||||||
const txHash = txResponse.transactionHash;
|
|
||||||
if (txHash) {
|
|
||||||
await this.updateProject(project.id, { txHash });
|
|
||||||
project.txHash = txHash;
|
|
||||||
log('Funds transferrend to deployer');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deploymentData = {
|
|
||||||
project,
|
|
||||||
branch: project.prodBranch,
|
|
||||||
environment: Environment.Production,
|
|
||||||
domain: null,
|
|
||||||
commitHash: latestCommit.sha,
|
|
||||||
commitMessage: latestCommit.commit.message,
|
|
||||||
deployer
|
|
||||||
};
|
|
||||||
|
|
||||||
const newDeployment = await this.createDeployment(user.id, octokit, deploymentData);
|
|
||||||
// Update project with deployer
|
|
||||||
await this.updateProjectWithDeployer(newDeployment.projectId, newDeployment.deployer);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.createRepoHook(octokit, project);
|
await this.createRepoHook(octokit, project);
|
||||||
|
|
||||||
|
console.log('projectid is', project.id);
|
||||||
|
|
||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -997,9 +782,6 @@ export class Service {
|
|||||||
);
|
);
|
||||||
const projects = await this.db.getProjects({
|
const projects = await this.db.getProjects({
|
||||||
where: { repository: repository.full_name },
|
where: { repository: repository.full_name },
|
||||||
relations: {
|
|
||||||
deployers: true,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!projects.length) {
|
if (!projects.length) {
|
||||||
@ -1016,29 +798,18 @@ export class Service {
|
|||||||
branch,
|
branch,
|
||||||
});
|
});
|
||||||
|
|
||||||
const deployers = project.deployers;
|
// Create deployment with branch and latest commit in GitHub data
|
||||||
if (!deployers) {
|
await this.createDeployment(project.ownerId, octokit, {
|
||||||
log(`No deployer present for project ${project.id}`)
|
project,
|
||||||
return;
|
branch,
|
||||||
}
|
environment:
|
||||||
|
project.prodBranch === branch
|
||||||
for (const deployer of deployers) {
|
? Environment.Production
|
||||||
// Create deployment with branch and latest commit in GitHub data
|
: Environment.Preview,
|
||||||
await this.createDeployment(project.ownerId, octokit,
|
domain,
|
||||||
{
|
commitHash: headCommit.id,
|
||||||
project,
|
commitMessage: headCommit.message,
|
||||||
branch,
|
});
|
||||||
environment:
|
|
||||||
project.prodBranch === branch
|
|
||||||
? Environment.Production
|
|
||||||
: Environment.Preview,
|
|
||||||
domain,
|
|
||||||
commitHash: headCommit.id,
|
|
||||||
commitMessage: headCommit.message,
|
|
||||||
deployer: deployer
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1075,7 +846,6 @@ export class Service {
|
|||||||
relations: {
|
relations: {
|
||||||
project: true,
|
project: true,
|
||||||
domain: true,
|
domain: true,
|
||||||
deployer: true,
|
|
||||||
createdBy: true,
|
createdBy: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
@ -1089,24 +859,15 @@ export class Service {
|
|||||||
|
|
||||||
const octokit = await this.getOctokit(user.id);
|
const octokit = await this.getOctokit(user.id);
|
||||||
|
|
||||||
let newDeployment: Deployment;
|
const newDeployment = await this.createDeployment(user.id, octokit, {
|
||||||
|
project: oldDeployment.project,
|
||||||
if (oldDeployment.project.auctionId) {
|
// TODO: Put isCurrent field in project
|
||||||
newDeployment = await this.createDeploymentFromAuction(oldDeployment.project, oldDeployment.deployer);
|
branch: oldDeployment.branch,
|
||||||
} else {
|
environment: Environment.Production,
|
||||||
newDeployment = await this.createDeployment(user.id, octokit,
|
domain: oldDeployment.domain,
|
||||||
{
|
commitHash: oldDeployment.commitHash,
|
||||||
project: oldDeployment.project,
|
commitMessage: oldDeployment.commitMessage,
|
||||||
// TODO: Put isCurrent field in project
|
});
|
||||||
branch: oldDeployment.branch,
|
|
||||||
environment: Environment.Production,
|
|
||||||
domain: oldDeployment.domain,
|
|
||||||
commitHash: oldDeployment.commitHash,
|
|
||||||
commitMessage: oldDeployment.commitMessage,
|
|
||||||
deployer: oldDeployment.deployer
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newDeployment;
|
return newDeployment;
|
||||||
}
|
}
|
||||||
@ -1152,18 +913,16 @@ export class Service {
|
|||||||
},
|
},
|
||||||
relations: {
|
relations: {
|
||||||
project: true,
|
project: true,
|
||||||
deployer: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deployment && deployment.applicationDeploymentRecordId) {
|
if (deployment && deployment.applicationDeploymentRecordId) {
|
||||||
// If deployment is current, remove deployment for project subdomain as well
|
// If deployment is current, remove deployment for project subdomain as well
|
||||||
if (deployment.isCurrent) {
|
if (deployment.isCurrent) {
|
||||||
const currentDeploymentURL = `https://${(deployment.project.name).toLowerCase()}.${deployment.deployer.baseDomain}`;
|
const currentDeploymentURL = `https://${deployment.project.subDomain}`;
|
||||||
|
|
||||||
// TODO: Store the latest DNS deployment record
|
|
||||||
const deploymentRecords =
|
const deploymentRecords =
|
||||||
await this.laconicRegistry.getDeploymentRecordsByFilter({
|
await this.registry.getDeploymentRecordsByFilter({
|
||||||
application: deployment.applicationRecordId,
|
application: deployment.applicationRecordId,
|
||||||
url: currentDeploymentURL,
|
url: currentDeploymentURL,
|
||||||
});
|
});
|
||||||
@ -1176,24 +935,14 @@ export class Service {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multiple records are fetched, take the latest record
|
await this.registry.createApplicationDeploymentRemovalRequest({
|
||||||
const latestRecord = deploymentRecords
|
deploymentId: deploymentRecords[0].id,
|
||||||
.sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime())[0];
|
|
||||||
|
|
||||||
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
|
|
||||||
deploymentId: latestRecord.id,
|
|
||||||
deployerLrn: deployment.deployer.deployerLrn,
|
|
||||||
auctionId: deployment.project.auctionId,
|
|
||||||
payment: deployment.project.txHash
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
|
await this.registry.createApplicationDeploymentRemovalRequest({
|
||||||
deploymentId: deployment.applicationDeploymentRecordId,
|
deploymentId: deployment.applicationDeploymentRecordId,
|
||||||
deployerLrn: deployment.deployer.deployerLrn,
|
|
||||||
auctionId: deployment.project.auctionId,
|
|
||||||
payment: deployment.project.txHash
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.db.updateDeploymentById(deployment.id, {
|
await this.db.updateDeploymentById(deployment.id, {
|
||||||
@ -1328,157 +1077,4 @@ export class Service {
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
return this.db.updateUser(user, data);
|
return this.db.updateUser(user, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEnvVariables(
|
|
||||||
projectId: string,
|
|
||||||
): Promise<{ [key: string]: string }> {
|
|
||||||
const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(projectId, {
|
|
||||||
environment: Environment.Production,
|
|
||||||
});
|
|
||||||
|
|
||||||
const environmentVariablesObj = environmentVariables.reduce(
|
|
||||||
(acc, env) => {
|
|
||||||
acc[env.key] = env.value;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as { [key: string]: string },
|
|
||||||
);
|
|
||||||
|
|
||||||
return environmentVariablesObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAuctionData(
|
|
||||||
auctionId: string
|
|
||||||
): Promise<any> {
|
|
||||||
const auctions = await this.laconicRegistry.getAuctionData(auctionId);
|
|
||||||
return auctions[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async releaseDeployerFundsByProjectId(projectId: string): Promise<boolean> {
|
|
||||||
const project = await this.db.getProjectById(projectId);
|
|
||||||
|
|
||||||
if (!project || !project.auctionId) {
|
|
||||||
log(`Project ${projectId} ${!project ? 'not found' : 'does not have an auction'}`);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auction = await this.laconicRegistry.releaseDeployerFunds(project.auctionId);
|
|
||||||
|
|
||||||
if (auction.auction.fundsReleased) {
|
|
||||||
log(`Funds released for auction ${project.auctionId}`);
|
|
||||||
await this.db.updateProjectById(projectId, { fundsReleased: true });
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`Error releasing funds for auction ${project.auctionId}`);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async returnUserFundsByProjectId(projectId: string, winningDeployersPresent: boolean) {
|
|
||||||
const project = await this.db.getProjectById(projectId);
|
|
||||||
|
|
||||||
if (!project || !project.auctionId) {
|
|
||||||
log(`Project ${projectId} ${!project ? 'not found' : 'does not have an auction'}`);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auction = await this.getAuctionData(project.auctionId);
|
|
||||||
const totalAuctionPrice = Number(auction.maxPrice.quantity) * auction.numProviders;
|
|
||||||
|
|
||||||
let amountToBeReturned;
|
|
||||||
if (winningDeployersPresent) {
|
|
||||||
amountToBeReturned = totalAuctionPrice - auction.winnerAddresses.length * Number(auction.winnerPrice.quantity);
|
|
||||||
} else {
|
|
||||||
amountToBeReturned = totalAuctionPrice;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (amountToBeReturned !== 0) {
|
|
||||||
await this.laconicRegistry.sendTokensToAccount(
|
|
||||||
project.paymentAddress,
|
|
||||||
amountToBeReturned.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDeployers(): Promise<Deployer[]> {
|
|
||||||
const dbDeployers = await this.db.getDeployers();
|
|
||||||
|
|
||||||
if (dbDeployers.length > 0) {
|
|
||||||
// Call asynchronously to fetch the records from the registry and update the DB
|
|
||||||
this.updateDeployersFromRegistry();
|
|
||||||
return dbDeployers;
|
|
||||||
} else {
|
|
||||||
// Fetch from the registry and populate empty DB
|
|
||||||
return await this.updateDeployersFromRegistry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateDeployersFromRegistry(): Promise<Deployer[]> {
|
|
||||||
const deployerRecords = await this.laconicRegistry.getDeployerRecordsByFilter({});
|
|
||||||
await this.saveDeployersByDeployerRecords(deployerRecords);
|
|
||||||
|
|
||||||
return await this.db.getDeployers();
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveDeployersByDeployerRecords(deployerRecords: DeployerRecord[]): Promise<Deployer[]> {
|
|
||||||
const deployers: Deployer[] = [];
|
|
||||||
|
|
||||||
for (const record of deployerRecords) {
|
|
||||||
if (record.names && record.names.length > 0) {
|
|
||||||
const deployerId = record.id;
|
|
||||||
const deployerLrn = record.names[0];
|
|
||||||
const deployerApiUrl = record.attributes.apiUrl;
|
|
||||||
const minimumPayment = record.attributes.minimumPayment;
|
|
||||||
const paymentAddress = record.attributes.paymentAddress;
|
|
||||||
const baseDomain = deployerApiUrl.substring(deployerApiUrl.indexOf('.') + 1);
|
|
||||||
|
|
||||||
const deployerData = {
|
|
||||||
deployerLrn,
|
|
||||||
deployerId,
|
|
||||||
deployerApiUrl,
|
|
||||||
baseDomain,
|
|
||||||
minimumPayment,
|
|
||||||
paymentAddress
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Update deployers table in a separate job
|
|
||||||
const deployer = await this.db.addDeployer(deployerData);
|
|
||||||
deployers.push(deployer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return deployers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAddress(): Promise<any> {
|
|
||||||
const account = await this.laconicRegistry.getAccount();
|
|
||||||
|
|
||||||
return account.address;
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyTx(txHash: string, amountSent: string, senderAddress: string): Promise<boolean> {
|
|
||||||
const txResponse = await this.laconicRegistry.getTxResponse(txHash);
|
|
||||||
if (!txResponse) {
|
|
||||||
log('Transaction response not found');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const transfer = txResponse.events.find(e => e.type === 'transfer' && e.attributes.some(a => a.key === 'msg_index'));
|
|
||||||
if (!transfer) {
|
|
||||||
log('No transfer event found');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sender = transfer.attributes.find(a => a.key === 'sender')?.value;
|
|
||||||
const recipient = transfer.attributes.find(a => a.key === 'recipient')?.value;
|
|
||||||
const amount = transfer.attributes.find(a => a.key === 'amount')?.value;
|
|
||||||
|
|
||||||
const recipientAddress = await this.getAddress();
|
|
||||||
|
|
||||||
return amount === amountSent && sender === senderAddress && recipient === recipientAddress;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -29,8 +29,6 @@ export interface GitPushEventPayload {
|
|||||||
|
|
||||||
export interface AppDeploymentRecordAttributes {
|
export interface AppDeploymentRecordAttributes {
|
||||||
application: string;
|
application: string;
|
||||||
auction: string;
|
|
||||||
deployer: string;
|
|
||||||
dns: string;
|
dns: string;
|
||||||
meta: string;
|
meta: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -70,35 +68,4 @@ export interface AddProjectFromTemplateInput {
|
|||||||
owner: string;
|
owner: string;
|
||||||
name: string;
|
name: string;
|
||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
paymentAddress: string;
|
|
||||||
txHash: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuctionParams {
|
|
||||||
maxPrice: string,
|
|
||||||
numProviders: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EnvironmentVariables {
|
|
||||||
environments: string[],
|
|
||||||
key: string,
|
|
||||||
value: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DeployerRecord {
|
|
||||||
id: string;
|
|
||||||
names: string[];
|
|
||||||
owners: string[];
|
|
||||||
bondId: string;
|
|
||||||
createTime: string;
|
|
||||||
expiryTime: string;
|
|
||||||
attributes: {
|
|
||||||
apiUrl: string;
|
|
||||||
minimumPayment: string | null;
|
|
||||||
name: string;
|
|
||||||
paymentAddress: string;
|
|
||||||
publicKey: string;
|
|
||||||
type: string;
|
|
||||||
version: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
import assert from 'assert';
|
|
||||||
import debug from 'debug';
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import { Octokit } from 'octokit';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import toml from 'toml';
|
import toml from 'toml';
|
||||||
|
import debug from 'debug';
|
||||||
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||||
|
|
||||||
import { Config } from './config';
|
import { Config } from './config';
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
||||||
import { PackageJSON } from './types';
|
|
||||||
|
|
||||||
const log = debug('snowball:utils');
|
const log = debug('snowball:utils');
|
||||||
|
|
||||||
@ -80,64 +76,3 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
|||||||
|
|
||||||
export const sleep = async (ms: number): Promise<void> =>
|
export const sleep = async (ms: number): Promise<void> =>
|
||||||
new Promise((resolve) => setTimeout(resolve, ms));
|
new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
export const getRepoDetails = async (
|
|
||||||
octokit: Octokit,
|
|
||||||
repository: string,
|
|
||||||
commitHash: string | undefined,
|
|
||||||
): Promise<{
|
|
||||||
repo: string;
|
|
||||||
packageJSON: PackageJSON;
|
|
||||||
repoUrl: string;
|
|
||||||
}> => {
|
|
||||||
const [owner, repo] = repository.split('/');
|
|
||||||
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
path: 'package.json',
|
|
||||||
ref: commitHash,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!packageJSONData) {
|
|
||||||
throw new Error('Package.json file not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
|
||||||
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
|
|
||||||
|
|
||||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
|
||||||
|
|
||||||
const repoUrl = (
|
|
||||||
await octokit.rest.repos.get({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
})
|
|
||||||
).data.html_url;
|
|
||||||
|
|
||||||
return {
|
|
||||||
repo,
|
|
||||||
packageJSON,
|
|
||||||
repoUrl
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrapper method for registry txs to retry once if 'account sequence mismatch' occurs
|
|
||||||
export const registryTransactionWithRetry = async (
|
|
||||||
txMethod: () => Promise<any>
|
|
||||||
): Promise<any> => {
|
|
||||||
try {
|
|
||||||
return await txMethod();
|
|
||||||
} catch (error: any) {
|
|
||||||
if (!error.message.includes('account sequence mismatch')) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`Transaction failed due to account sequence mismatch. Retrying...`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await txMethod();
|
|
||||||
} catch (retryError: any) {
|
|
||||||
throw new Error(`Transaction failed again after retry: ${retryError.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -2,6 +2,8 @@ import * as fs from 'fs/promises';
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
|
||||||
import { getConfig } from '../src/utils';
|
import { getConfig } from '../src/utils';
|
||||||
|
import { Config } from '../src/config';
|
||||||
|
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||||
|
|
||||||
const log = debug('snowball:delete-database');
|
const log = debug('snowball:delete-database');
|
||||||
|
|
||||||
@ -11,7 +13,7 @@ const deleteFile = async (filePath: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const config = await getConfig();
|
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||||
|
|
||||||
deleteFile(config.database.dbPath);
|
deleteFile(config.database.dbPath);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
||||||
"name": "Deploy Tools",
|
"name": "Snowball Tools",
|
||||||
"slug": "deploy-tools"
|
"slug": "snowball-tools-1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
|
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
|
||||||
|
@ -16,6 +16,8 @@ import {
|
|||||||
getEntities,
|
getEntities,
|
||||||
loadAndSaveData
|
loadAndSaveData
|
||||||
} from '../src/utils';
|
} from '../src/utils';
|
||||||
|
import { Config } from '../src/config';
|
||||||
|
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||||
|
|
||||||
const log = debug('snowball:initialize-database');
|
const log = debug('snowball:initialize-database');
|
||||||
|
|
||||||
@ -154,7 +156,7 @@ const generateTestData = async (dataSource: DataSource) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const config = await getConfig();
|
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||||
const isDbPresent = await checkFileExists(config.database.dbPath);
|
const isDbPresent = await checkFileExists(config.database.dbPath);
|
||||||
|
|
||||||
if (!isDbPresent) {
|
if (!isDbPresent) {
|
||||||
|
@ -1,40 +1,39 @@
|
|||||||
import debug from 'debug';
|
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';
|
import { getConfig } from '../src/utils';
|
||||||
|
|
||||||
const log = debug('snowball:initialize-registry');
|
const log = debug('snowball:initialize-registry');
|
||||||
|
|
||||||
const DENOM = 'alnt';
|
const DENOM = 'aphoton';
|
||||||
const BOND_AMOUNT = '1000000000';
|
const BOND_AMOUNT = '1000000000';
|
||||||
|
|
||||||
async function main () {
|
async function main () {
|
||||||
const { registryConfig } = await getConfig();
|
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||||
|
|
||||||
// TODO: Get authority names from args
|
// TODO: Get authority names from args
|
||||||
const authorityNames = ['snowballtools', registryConfig.authority];
|
const authorityNames = ['snowballtools', registryConfig.authority];
|
||||||
|
|
||||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, {chainId: registryConfig.chainId});
|
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
||||||
|
|
||||||
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
||||||
log('bondId:', bondId);
|
log('bondId:', bondId);
|
||||||
|
|
||||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
|
||||||
|
|
||||||
await registry.createBond(
|
await registry.createBond(
|
||||||
{ denom: DENOM, amount: BOND_AMOUNT },
|
{ denom: DENOM, amount: BOND_AMOUNT },
|
||||||
registryConfig.privateKey,
|
registryConfig.privateKey,
|
||||||
fee
|
registryConfig.fee
|
||||||
);
|
);
|
||||||
|
|
||||||
for await (const name of authorityNames) {
|
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);
|
log('Reserved authority name:', name);
|
||||||
await registry.setAuthorityBond(
|
await registry.setAuthorityBond(
|
||||||
{ name, bondId },
|
{ name, bondId },
|
||||||
registryConfig.privateKey,
|
registryConfig.privateKey,
|
||||||
fee
|
registryConfig.fee
|
||||||
);
|
);
|
||||||
log(`Bond ${bondId} set for authority ${name}`);
|
log(`Bond ${bondId} set for authority ${name}`);
|
||||||
}
|
}
|
||||||
|
@ -2,20 +2,22 @@ import debug from 'debug';
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import path from 'path';
|
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 { getConfig } from '../src/utils';
|
||||||
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
|
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
|
||||||
|
|
||||||
const log = debug('snowball:publish-deploy-records');
|
const log = debug('snowball:publish-deploy-records');
|
||||||
|
|
||||||
async function main() {
|
async function main () {
|
||||||
const { registryConfig, database, misc } = await getConfig();
|
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||||
|
|
||||||
const registry = new Registry(
|
const registry = new Registry(
|
||||||
registryConfig.gqlEndpoint,
|
registryConfig.gqlEndpoint,
|
||||||
registryConfig.restEndpoint,
|
registryConfig.restEndpoint,
|
||||||
{ chainId: registryConfig.chainId }
|
registryConfig.chainId
|
||||||
);
|
);
|
||||||
|
|
||||||
const dataSource = new DataSource({
|
const dataSource = new DataSource({
|
||||||
@ -38,7 +40,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for await (const deployment of deployments) {
|
for await (const deployment of deployments) {
|
||||||
const url = `https://${(deployment.project.name).toLowerCase()}-${deployment.id}.${deployment.deployer.baseDomain}`;
|
const url = `https://${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
|
||||||
|
|
||||||
const applicationDeploymentRecord = {
|
const applicationDeploymentRecord = {
|
||||||
type: 'ApplicationDeploymentRecord',
|
type: 'ApplicationDeploymentRecord',
|
||||||
@ -59,8 +61,6 @@ async function main() {
|
|||||||
url
|
url
|
||||||
};
|
};
|
||||||
|
|
||||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
|
||||||
|
|
||||||
const result = await registry.setRecord(
|
const result = await registry.setRecord(
|
||||||
{
|
{
|
||||||
privateKey: registryConfig.privateKey,
|
privateKey: registryConfig.privateKey,
|
||||||
@ -68,12 +68,12 @@ async function main() {
|
|||||||
bondId: registryConfig.bondId
|
bondId: registryConfig.bondId
|
||||||
},
|
},
|
||||||
'',
|
'',
|
||||||
fee
|
registryConfig.fee
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove deployment for project subdomain if deployment is for production environment
|
// Remove deployment for project subdomain if deployment is for production environment
|
||||||
if (deployment.environment === Environment.Production) {
|
if (deployment.environment === Environment.Production) {
|
||||||
applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.deployer.baseDomain}`;
|
applicationDeploymentRecord.url = `https://${deployment.project.subDomain}`
|
||||||
|
|
||||||
await registry.setRecord(
|
await registry.setRecord(
|
||||||
{
|
{
|
||||||
@ -82,12 +82,12 @@ async function main() {
|
|||||||
bondId: registryConfig.bondId
|
bondId: registryConfig.bondId
|
||||||
},
|
},
|
||||||
'',
|
'',
|
||||||
fee
|
registryConfig.fee
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
log('Application deployment record data:', applicationDeploymentRecord);
|
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 { DataSource } from 'typeorm';
|
||||||
import path from 'path';
|
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 { getConfig } from '../src/utils';
|
||||||
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
||||||
|
|
||||||
const log = debug('snowball:publish-deployment-removal-records');
|
const log = debug('snowball:publish-deployment-removal-records');
|
||||||
|
|
||||||
async function main () {
|
async function main () {
|
||||||
const { registryConfig, database } = await getConfig();
|
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||||
|
|
||||||
const registry = new Registry(
|
const registry = new Registry(
|
||||||
registryConfig.gqlEndpoint,
|
registryConfig.gqlEndpoint,
|
||||||
registryConfig.restEndpoint,
|
registryConfig.restEndpoint,
|
||||||
{ chainId: registryConfig.chainId }
|
registryConfig.chainId
|
||||||
);
|
);
|
||||||
|
|
||||||
const dataSource = new DataSource({
|
const dataSource = new DataSource({
|
||||||
@ -45,8 +47,6 @@ async function main () {
|
|||||||
request: deployment.applicationDeploymentRemovalRequestId,
|
request: deployment.applicationDeploymentRemovalRequestId,
|
||||||
}
|
}
|
||||||
|
|
||||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
|
||||||
|
|
||||||
const result = await registry.setRecord(
|
const result = await registry.setRecord(
|
||||||
{
|
{
|
||||||
privateKey: registryConfig.privateKey,
|
privateKey: registryConfig.privateKey,
|
||||||
@ -54,11 +54,11 @@ async function main () {
|
|||||||
bondId: registryConfig.bondId
|
bondId: registryConfig.bondId
|
||||||
},
|
},
|
||||||
'',
|
'',
|
||||||
fee
|
registryConfig.fee
|
||||||
);
|
);
|
||||||
|
|
||||||
log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
|
log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
|
||||||
log(`Application deployment removal record published: ${result.id}`);
|
log(`Application deployment removal record published: ${result.data.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
REGISTRY_BOND_ID=
|
|
||||||
DEPLOYER_LRN=
|
|
||||||
AUTHORITY=
|
|
@ -51,14 +51,14 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Account balance
|
# Account balance
|
||||||
yarn laconic registry account get
|
yarn laconic cns account get
|
||||||
|
|
||||||
# Bond balance
|
# Bond balance
|
||||||
yarn laconic registry bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||||
```
|
```
|
||||||
|
|
||||||
- Command to refill bond
|
- Command to refill bond
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn laconic registry bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type alnt --quantity 10000000
|
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
|
||||||
```
|
```
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
registry:
|
cns:
|
||||||
restEndpoint: 'http://console.laconic.com:1317'
|
restEndpoint: 'http://console.laconic.com:1317'
|
||||||
gqlEndpoint: 'http://console.laconic.com:9473/api'
|
gqlEndpoint: 'http://console.laconic.com:9473/api'
|
||||||
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
|
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
|
||||||
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
|
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
|
||||||
chainId: laconic_9000-1
|
chainId: laconic_9000-1
|
||||||
gas:
|
gas: 1200000
|
||||||
fees:
|
fees: 200000aphoton
|
||||||
gasPrice: 1
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
registry:
|
cns:
|
||||||
rpcEndpoint: https://laconicd-sapo.laconic.com
|
restEndpoint: http://console.laconic.com:1317
|
||||||
gqlEndpoint: https://laconicd-sapo.laconic.com/api
|
gqlEndpoint: http://console.laconic.com:9473/api
|
||||||
userKey:
|
userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
|
||||||
bondId:
|
bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||||
chainId: laconic_9000-2
|
chainId: laconic_9000-1
|
||||||
gasPrice: 1alnt
|
gas: 1200000
|
||||||
|
fees: 200000aphoton
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
source .env
|
|
||||||
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
|
||||||
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
|
||||||
echo "Using AUTHORITY: $AUTHORITY"
|
|
||||||
|
|
||||||
# Repository URL
|
# Repository URL
|
||||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||||
|
|
||||||
@ -18,17 +13,40 @@ PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
|||||||
CURRENT_DATE_TIME=$(date -u)
|
CURRENT_DATE_TIME=$(date -u)
|
||||||
|
|
||||||
CONFIG_FILE=config.yml
|
CONFIG_FILE=config.yml
|
||||||
|
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
|
||||||
|
|
||||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||||
|
|
||||||
# Get latest version from registry and increment application-record version
|
# Get latest version from registry and increment application-record version
|
||||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "deploy-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
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
|
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||||
# Set application-record version if no previous records were found
|
# Set application-record version if no previous records were found
|
||||||
NEW_APPLICATION_VERSION=0.0.1
|
NEW_APPLICATION_VERSION=0.0.1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Generate application-deployment-request.yml
|
||||||
|
cat >./records/application-deployment-request.yml <<EOF
|
||||||
|
record:
|
||||||
|
type: ApplicationDeploymentRequest
|
||||||
|
version: '1.0.0'
|
||||||
|
name: snowballtools-base-frontend@$PACKAGE_VERSION
|
||||||
|
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
|
||||||
|
dns: dashboard
|
||||||
|
config:
|
||||||
|
env:
|
||||||
|
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||||
|
LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab
|
||||||
|
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/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_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||||
|
meta:
|
||||||
|
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||||
|
repository: "$REPO_URL"
|
||||||
|
repository_ref: $LATEST_HASH
|
||||||
|
EOF
|
||||||
|
|
||||||
# Generate application-record.yml with incremented version
|
# Generate application-record.yml with incremented version
|
||||||
cat >./records/application-record.yml <<EOF
|
cat >./records/application-record.yml <<EOF
|
||||||
record:
|
record:
|
||||||
@ -37,16 +55,16 @@ record:
|
|||||||
repository_ref: $LATEST_HASH
|
repository_ref: $LATEST_HASH
|
||||||
repository: ["$REPO_URL"]
|
repository: ["$REPO_URL"]
|
||||||
app_type: webapp
|
app_type: webapp
|
||||||
name: deploy-frontend
|
name: snowballtools-base-frontend
|
||||||
app_version: $PACKAGE_VERSION
|
app_version: $PACKAGE_VERSION
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Files generated successfully"
|
echo "Files generated successfully."
|
||||||
|
|
||||||
RECORD_FILE=records/application-record.yml
|
RECORD_FILE=records/application-record.yml
|
||||||
|
|
||||||
# Publish ApplicationRecord
|
# Publish ApplicationRecord
|
||||||
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
echo "FATAL: Failed to publish record"
|
echo "FATAL: Failed to publish record"
|
||||||
@ -57,17 +75,17 @@ echo "ApplicationRecord published"
|
|||||||
echo $RECORD_ID
|
echo $RECORD_ID
|
||||||
|
|
||||||
# Set name to record
|
# Set name to record
|
||||||
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
|
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
|
||||||
|
|
||||||
sleep 2
|
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"
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
echo "FATAL: Failed to set name: $REGISTRY_APP_CRN@${PACKAGE_VERSION}"
|
||||||
exit $rc
|
exit $rc
|
||||||
fi
|
fi
|
||||||
sleep 2
|
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"
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
echo "FATAL: Failed to set hash"
|
echo "FATAL: Failed to set hash"
|
||||||
@ -75,16 +93,16 @@ if [ $rc -ne 0 ]; then
|
|||||||
fi
|
fi
|
||||||
sleep 2
|
sleep 2
|
||||||
# Set name if latest release
|
# Set name if latest release
|
||||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
echo "FATAL: Failed to set release"
|
echo "FATAL: Failed to set release"
|
||||||
exit $rc
|
exit $rc
|
||||||
fi
|
fi
|
||||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||||
|
|
||||||
# Check if record found for REGISTRY_APP_LRN
|
# Check if record found for REGISTRY_APP_CRN
|
||||||
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN")
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
echo "FATAL: Failed to query name"
|
echo "FATAL: Failed to query name"
|
||||||
@ -92,53 +110,14 @@ if [ $rc -ne 0 ]; then
|
|||||||
fi
|
fi
|
||||||
APP_RECORD=$(echo $query_response | jq '.[0]')
|
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||||
echo "No record found for $REGISTRY_APP_LRN."
|
echo "No record found for $REGISTRY_APP_CRN."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get payment address for deployer
|
|
||||||
paymentAddress=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.paymentAddress')
|
|
||||||
paymentAmount=$(yarn --silent laconic -c config.yml registry name resolve "$DEPLOYER_LRN" | jq -r '.[0].attributes.minimumPayment' | sed 's/alnt//g')
|
|
||||||
# Pay deployer if paymentAmount is not null
|
|
||||||
if [[ -n "$paymentAmount" && "$paymentAmount" != "null" ]]; then
|
|
||||||
payment=$(yarn --silent laconic -c config.yml registry tokens send --address "$paymentAddress" --type alnt --quantity "$paymentAmount")
|
|
||||||
|
|
||||||
# Extract the transaction hash
|
|
||||||
txHash=$(echo "$payment" | jq -r '.tx.hash')
|
|
||||||
echo "Paid deployer with txHash as $txHash"
|
|
||||||
|
|
||||||
else
|
|
||||||
echo "Payment amount is null; skipping payment."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Generate application-deployment-request.yml
|
|
||||||
cat >./records/application-deployment-request.yml <<EOF
|
|
||||||
record:
|
|
||||||
type: ApplicationDeploymentRequest
|
|
||||||
version: '1.0.0'
|
|
||||||
name: deploy-frontend@$PACKAGE_VERSION
|
|
||||||
application: lrn://$AUTHORITY/applications/deploy-frontend@$PACKAGE_VERSION
|
|
||||||
deployer: $DEPLOYER_LRN
|
|
||||||
dns: deploy
|
|
||||||
config:
|
|
||||||
env:
|
|
||||||
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.apps.vaasl.io
|
|
||||||
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liaet4yc0KX0iM1c
|
|
||||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
|
||||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
|
||||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
|
|
||||||
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
|
||||||
meta:
|
|
||||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
|
||||||
repository: "$REPO_URL"
|
|
||||||
repository_ref: $LATEST_HASH
|
|
||||||
payment: $txHash
|
|
||||||
EOF
|
|
||||||
|
|
||||||
RECORD_FILE=records/application-deployment-request.yml
|
RECORD_FILE=records/application-deployment-request.yml
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
echo "FATAL: Failed to query deployment request"
|
echo "FATAL: Failed to query deployment request"
|
||||||
exit $rc
|
exit $rc
|
||||||
|
@ -18,7 +18,7 @@ REGISTRY_BOND_ID="098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb466
|
|||||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||||
|
|
||||||
# Get latest version from registry and increment application-record version
|
# Get latest version from registry and increment application-record version
|
||||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE 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
|
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||||
# Set application-record version if no previous records were found
|
# Set application-record version if no previous records were found
|
||||||
@ -31,7 +31,7 @@ record:
|
|||||||
type: ApplicationDeploymentRequest
|
type: ApplicationDeploymentRequest
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
name: staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
name: staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
||||||
application: 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
|
dns: dashboard.staging.apps.snowballtools.com
|
||||||
config:
|
config:
|
||||||
env:
|
env:
|
||||||
@ -41,7 +41,6 @@ record:
|
|||||||
LACONIC_HOSTED_CONFIG_github_pwa_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_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
|
||||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||||
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
|
|
||||||
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
|
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
|
||||||
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
||||||
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
||||||
@ -71,7 +70,7 @@ echo "Files generated successfully."
|
|||||||
RECORD_FILE=staging-records/application-record.yml
|
RECORD_FILE=staging-records/application-record.yml
|
||||||
|
|
||||||
# Publish ApplicationRecord
|
# Publish ApplicationRecord
|
||||||
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
echo "FATAL: Failed to publish record"
|
echo "FATAL: Failed to publish record"
|
||||||
@ -82,17 +81,17 @@ echo "ApplicationRecord published"
|
|||||||
echo $RECORD_ID
|
echo $RECORD_ID
|
||||||
|
|
||||||
# Set name to record
|
# 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
|
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"
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
|
echo "FATAL: Failed to set name: $REGISTRY_APP_CRN@${PACKAGE_VERSION}"
|
||||||
exit $rc
|
exit $rc
|
||||||
fi
|
fi
|
||||||
sleep 2
|
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"
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
echo "FATAL: Failed to set hash"
|
echo "FATAL: Failed to set hash"
|
||||||
@ -100,16 +99,16 @@ if [ $rc -ne 0 ]; then
|
|||||||
fi
|
fi
|
||||||
sleep 2
|
sleep 2
|
||||||
# Set name if latest release
|
# Set name if latest release
|
||||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
echo "FATAL: Failed to set release"
|
echo "FATAL: Failed to set release"
|
||||||
exit $rc
|
exit $rc
|
||||||
fi
|
fi
|
||||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||||
|
|
||||||
# Check if record found for REGISTRY_APP_LRN
|
# Check if record found for REGISTRY_APP_CRN
|
||||||
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN")
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
echo "FATAL: Failed to query name"
|
echo "FATAL: Failed to query name"
|
||||||
@ -117,14 +116,14 @@ if [ $rc -ne 0 ]; then
|
|||||||
fi
|
fi
|
||||||
APP_RECORD=$(echo $query_response | jq '.[0]')
|
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||||
echo "No record found for $REGISTRY_APP_LRN."
|
echo "No record found for $REGISTRY_APP_CRN."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
RECORD_FILE=staging-records/application-deployment-request.yml
|
RECORD_FILE=staging-records/application-deployment-request.yml
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
|
||||||
if [ $rc -ne 0 ]; then
|
if [ $rc -ne 0 ]; then
|
||||||
echo "FATAL: Failed to query deployment request"
|
echo "FATAL: Failed to query deployment request"
|
||||||
exit $rc
|
exit $rc
|
||||||
|
@ -4,6 +4,6 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cerc-io/laconic-registry-cli": "^0.2.9"
|
"@snowballtools/laconic-registry-cli": "^0.1.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
record:
|
record:
|
||||||
type: ApplicationDeploymentRequest
|
type: ApplicationDeploymentRequest
|
||||||
version: '1.0.0'
|
version: "1.0.0"
|
||||||
name: deploy-frontend@1.0.0
|
name: snowballtools-base-frontend@0.1.8
|
||||||
application: lrn://vaasl/applications/deploy-frontend@1.0.0
|
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
|
||||||
dns: deploy
|
dns: dashboard
|
||||||
config:
|
config:
|
||||||
env:
|
env:
|
||||||
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.apps.vaasl.io
|
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||||
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liaet4yc0KX0iM1c
|
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
||||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
|
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools/test-progressive-web-app
|
||||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
|
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
|
||||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
|
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
|
||||||
|
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||||
meta:
|
meta:
|
||||||
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
|
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
|
||||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||||
|
@ -4,5 +4,5 @@ record:
|
|||||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
||||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||||
app_type: webapp
|
app_type: webapp
|
||||||
name: deploy-frontend
|
name: snowballtools-base-frontend
|
||||||
app_version: 1.0.0
|
app_version: 0.1.8
|
||||||
|
@ -19,6 +19,6 @@ record:
|
|||||||
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
|
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
|
||||||
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
|
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
|
||||||
meta:
|
meta:
|
||||||
note: Added by Snowball @ Mon Jun 24 23:51:48 UTC 2024
|
note: Added by Snowball @ Sat Jun 22 02:49:45 UTC 2024
|
||||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||||
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
|
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
|
||||||
|
@ -22,7 +22,7 @@ REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be
|
|||||||
APP_NAME=deployment-test-app
|
APP_NAME=deployment-test-app
|
||||||
|
|
||||||
# Get latest version from registry and increment application-record version
|
# Get latest version from registry and increment application-record version
|
||||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE 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
|
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||||
# Set application-record version if no previous records were found
|
# Set application-record version if no previous records were found
|
||||||
@ -51,7 +51,7 @@ record:
|
|||||||
type: ApplicationDeploymentRequest
|
type: ApplicationDeploymentRequest
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
name: $APP_NAME@$PACKAGE_VERSION
|
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
|
dns: deployment-ci-test
|
||||||
config:
|
config:
|
||||||
env:
|
env:
|
||||||
@ -67,31 +67,31 @@ EOF
|
|||||||
echo "Record files generated successfully."
|
echo "Record files generated successfully."
|
||||||
|
|
||||||
# Publish ApplicationRecord
|
# Publish ApplicationRecord
|
||||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE 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 "ApplicationRecord published"
|
||||||
echo $RECORD_ID
|
echo $RECORD_ID
|
||||||
|
|
||||||
# Set name to record
|
# Set name to record
|
||||||
REGISTRY_APP_LRN="lrn://snowballtools/applications/$APP_NAME"
|
REGISTRY_APP_CRN="crn://snowballtools/applications/$APP_NAME"
|
||||||
|
|
||||||
sleep 2
|
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
|
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
|
sleep 2
|
||||||
# Set name if latest release
|
# Set name if latest release
|
||||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||||
|
|
||||||
# Check if record exists for REGISTRY_APP_LRN
|
# Check if record exists for REGISTRY_APP_CRN
|
||||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN" | jq '.[0]')
|
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
||||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||||
echo "No record found for $REGISTRY_APP_LRN."
|
echo "No record found for $REGISTRY_APP_CRN."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 2
|
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 "ApplicationDeploymentRequest published"
|
||||||
echo $DEPLOYMENT_REQUEST_ID
|
echo $DEPLOYMENT_REQUEST_ID
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ MAX_RETRIES=20
|
|||||||
# Check that a ApplicationDeploymentRecord is published
|
# Check that a ApplicationDeploymentRecord is published
|
||||||
retry_count=0
|
retry_count=0
|
||||||
while true; do
|
while true; do
|
||||||
deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE 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')
|
len_deployment_records=$(echo $deployment_records_response | jq 'length')
|
||||||
|
|
||||||
# Check if number of records returned is 0
|
# Check if number of records returned is 0
|
||||||
@ -140,7 +140,7 @@ fetched_url=$(echo $deployment_records_response | jq -r '.[0].attributes.url')
|
|||||||
|
|
||||||
retry_count=0
|
retry_count=0
|
||||||
max_retries=10
|
max_retries=10
|
||||||
retry_interval=10
|
retry_interval=5
|
||||||
while true; do
|
while true; do
|
||||||
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
||||||
if [ "$url_response" = "200" ]; then
|
if [ "$url_response" = "200" ]; then
|
||||||
@ -170,14 +170,14 @@ record:
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
sleep 2
|
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 "ApplicationDeploymentRemovalRequest published"
|
||||||
echo $REMOVAL_REQUEST_ID
|
echo $REMOVAL_REQUEST_ID
|
||||||
|
|
||||||
# Check that an ApplicationDeploymentRemovalRecord is published
|
# Check that an ApplicationDeploymentRemovalRecord is published
|
||||||
retry_count=0
|
retry_count=0
|
||||||
while true; do
|
while true; do
|
||||||
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE 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')
|
len_removal_records=$(echo $removal_records_response | jq 'length')
|
||||||
|
|
||||||
# Check if number of records returned is 0
|
# Check if number of records returned is 0
|
||||||
|
@ -15,5 +15,3 @@ VITE_BUGSNAG_API_KEY=
|
|||||||
VITE_PASSKEY_WALLET_RPID=
|
VITE_PASSKEY_WALLET_RPID=
|
||||||
VITE_TURNKEY_API_BASE_URL=
|
VITE_TURNKEY_API_BASE_URL=
|
||||||
VITE_TURNKEY_ORGANIZATION_ID=
|
VITE_TURNKEY_ORGANIZATION_ID=
|
||||||
|
|
||||||
VITE_LACONICD_CHAIN_ID=
|
|
||||||
|
@ -44,7 +44,7 @@ yarn dev
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
From the root of the project,
|
From the root of the project, run:
|
||||||
|
|
||||||
### Staging
|
### Staging
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 3000",
|
"dev": "vite --port 3000",
|
||||||
@ -16,11 +16,8 @@
|
|||||||
"@bugsnag/browser-performance": "^2.4.1",
|
"@bugsnag/browser-performance": "^2.4.1",
|
||||||
"@bugsnag/js": "^7.22.7",
|
"@bugsnag/js": "^7.22.7",
|
||||||
"@bugsnag/plugin-react": "^7.22.7",
|
"@bugsnag/plugin-react": "^7.22.7",
|
||||||
"@emotion/react": "^11.13.3",
|
|
||||||
"@emotion/styled": "^11.13.0",
|
|
||||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||||
"@fontsource/inter": "^5.0.16",
|
"@fontsource/inter": "^5.0.16",
|
||||||
"@mui/material": "^6.1.3",
|
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
@ -30,6 +27,10 @@
|
|||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
|
"@snowballtools/auth": "^0.2.0",
|
||||||
|
"@snowballtools/auth-lit": "^0.2.0",
|
||||||
|
"@snowballtools/js-sdk": "^0.1.1",
|
||||||
|
"@snowballtools/link-lit-alchemy-light": "^0.2.0",
|
||||||
"@snowballtools/material-tailwind-react-fork": "^2.1.10",
|
"@snowballtools/material-tailwind-react-fork": "^2.1.10",
|
||||||
"@snowballtools/smartwallet-alchemy-light": "^0.2.0",
|
"@snowballtools/smartwallet-alchemy-light": "^0.2.0",
|
||||||
"@snowballtools/types": "^0.2.0",
|
"@snowballtools/types": "^0.2.0",
|
||||||
@ -42,8 +43,8 @@
|
|||||||
"@turnkey/sdk-react": "^0.1.0",
|
"@turnkey/sdk-react": "^0.1.0",
|
||||||
"@turnkey/webauthn-stamper": "^0.5.0",
|
"@turnkey/webauthn-stamper": "^0.5.0",
|
||||||
"@walletconnect/ethereum-provider": "^2.12.2",
|
"@walletconnect/ethereum-provider": "^2.12.2",
|
||||||
"@web3modal/siwe": "4.0.5",
|
"@web3modal/siwe": "^4.0.5",
|
||||||
"@web3modal/wagmi": "4.0.5",
|
"@web3modal/wagmi": "^4.0.5",
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
@ -61,15 +62,15 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropdown": "^1.11.0",
|
"react-dropdown": "^1.11.0",
|
||||||
"react-hook-form": "^7.49.0",
|
"react-hook-form": "^7.49.0",
|
||||||
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-oauth-popup": "^1.0.5",
|
"react-oauth-popup": "^1.0.5",
|
||||||
"react-router-dom": "^6.20.1",
|
"react-router-dom": "^6.20.1",
|
||||||
"react-timer-hook": "^3.0.7",
|
"react-timer-hook": "^3.0.7",
|
||||||
"siwe": "2.1.4",
|
"siwe": "^2.1.4",
|
||||||
"tailwind-variants": "^0.2.0",
|
"tailwind-variants": "^0.2.0",
|
||||||
"usehooks-ts": "^2.15.1",
|
"usehooks-ts": "^2.15.1",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"viem": "^2.7.11",
|
"viem": "^2.7.11",
|
||||||
"wagmi": "2.5.7",
|
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1 +0,0 @@
|
|||||||
350e9ac2-8b27-4a79-9a82-78cfdb68ef71=0eacb7ae462f82c8b0199d28193b0bfa5265973dbb1fe991eec2cab737dfc1ec
|
|
@ -12,7 +12,7 @@ import Index from './pages';
|
|||||||
import AuthPage from './pages/AuthPage';
|
import AuthPage from './pages/AuthPage';
|
||||||
import { DashboardLayout } from './pages/org-slug/layout';
|
import { DashboardLayout } from './pages/org-slug/layout';
|
||||||
import Web3Provider from 'context/Web3Provider';
|
import Web3Provider from 'context/Web3Provider';
|
||||||
import { BASE_URL } from 'utils/constants';
|
import { baseUrl } from 'utils/constants';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -50,26 +50,25 @@ const router = createBrowserRouter([
|
|||||||
path: '/login',
|
path: '/login',
|
||||||
element: <AuthPage />,
|
element: <AuthPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/signup',
|
||||||
|
element: <AuthPage />,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// Hacky way of checking session
|
// Hacky way of checking session
|
||||||
// TODO: Handle redirect backs
|
// TODO: Handle redirect backs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`${BASE_URL}/auth/session`, {
|
fetch(`${baseUrl}/auth/session`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
const path = window.location.pathname;
|
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
const path = window.location.pathname;
|
||||||
if (path !== '/login') {
|
if (path !== '/login' && path !== '/signup') {
|
||||||
window.location.pathname = '/login';
|
window.location.pathname = '/login';
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (path === '/login') {
|
|
||||||
window.location.pathname = '/';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useStopwatch } from 'react-timer-hook';
|
import { useStopwatch } from 'react-timer-hook';
|
||||||
|
|
||||||
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
|
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
|
||||||
@ -13,19 +12,14 @@ const setStopWatchOffset = (time: string) => {
|
|||||||
|
|
||||||
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
||||||
offsetTimestamp: Date;
|
offsetTimestamp: Date;
|
||||||
isPaused: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => {
|
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => {
|
||||||
const { totalSeconds, pause, start } = useStopwatch({
|
const { totalSeconds } = useStopwatch({
|
||||||
autoStart: true,
|
autoStart: true,
|
||||||
offsetTimestamp: offsetTimestamp,
|
offsetTimestamp: offsetTimestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
isPaused ? pause() : start();
|
|
||||||
}, [isPaused]);
|
|
||||||
|
|
||||||
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,16 +10,11 @@ import {
|
|||||||
LinkChainIcon,
|
LinkChainIcon,
|
||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import { TagProps } from 'components/shared/Tag';
|
import { TagProps } from 'components/shared/Tag';
|
||||||
import {
|
|
||||||
ArrowRightCircleFilledIcon,
|
|
||||||
LoadingIcon,
|
|
||||||
} from 'components/shared/CustomIcon';
|
|
||||||
|
|
||||||
interface ChangeStateToProductionDialogProps extends ConfirmDialogProps {
|
interface ChangeStateToProductionDialogProps extends ConfirmDialogProps {
|
||||||
deployment: Deployment;
|
deployment: Deployment;
|
||||||
newDeployment?: Deployment;
|
newDeployment?: Deployment;
|
||||||
domains: Domain[];
|
domains: Domain[];
|
||||||
isConfirmButtonLoading?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChangeStateToProductionDialog = ({
|
export const ChangeStateToProductionDialog = ({
|
||||||
@ -29,7 +24,6 @@ export const ChangeStateToProductionDialog = ({
|
|||||||
open,
|
open,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
handleConfirm,
|
handleConfirm,
|
||||||
isConfirmButtonLoading,
|
|
||||||
...props
|
...props
|
||||||
}: ChangeStateToProductionDialogProps) => {
|
}: ChangeStateToProductionDialogProps) => {
|
||||||
const currentChip = {
|
const currentChip = {
|
||||||
@ -47,14 +41,6 @@ export const ChangeStateToProductionDialog = ({
|
|||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
open={open}
|
open={open}
|
||||||
handleConfirm={handleConfirm}
|
handleConfirm={handleConfirm}
|
||||||
confirmButtonProps={{
|
|
||||||
disabled: isConfirmButtonLoading,
|
|
||||||
rightIcon: isConfirmButtonLoading ? (
|
|
||||||
<LoadingIcon className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<ArrowRightCircleFilledIcon />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-7">
|
<div className="flex flex-col gap-7">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
import ConfirmDialog, {
|
|
||||||
ConfirmDialogProps,
|
|
||||||
} from 'components/shared/ConfirmDialog';
|
|
||||||
import {
|
|
||||||
ArrowRightCircleFilledIcon,
|
|
||||||
LoadingIcon,
|
|
||||||
} from 'components/shared/CustomIcon';
|
|
||||||
|
|
||||||
interface DeleteDeploymentDialogProps extends ConfirmDialogProps {
|
|
||||||
isConfirmButtonLoading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeleteDeploymentDialog = ({
|
|
||||||
open,
|
|
||||||
handleCancel,
|
|
||||||
handleConfirm,
|
|
||||||
isConfirmButtonLoading,
|
|
||||||
...props
|
|
||||||
}: DeleteDeploymentDialogProps) => {
|
|
||||||
return (
|
|
||||||
<ConfirmDialog
|
|
||||||
{...props}
|
|
||||||
dialogTitle="Delete deployment?"
|
|
||||||
handleCancel={handleCancel}
|
|
||||||
open={open}
|
|
||||||
confirmButtonTitle={
|
|
||||||
isConfirmButtonLoading
|
|
||||||
? 'Deleting deployment'
|
|
||||||
: 'Yes, delete deployment'
|
|
||||||
}
|
|
||||||
handleConfirm={handleConfirm}
|
|
||||||
confirmButtonProps={{
|
|
||||||
variant: 'danger',
|
|
||||||
disabled: isConfirmButtonLoading,
|
|
||||||
rightIcon: isConfirmButtonLoading ? (
|
|
||||||
<LoadingIcon className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<ArrowRightCircleFilledIcon />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p className="text-sm text-elements-high-em">
|
|
||||||
Once deleted, the deployment will not be accessible.
|
|
||||||
</p>
|
|
||||||
</ConfirmDialog>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useCombobox } from 'downshift';
|
import { useCombobox } from 'downshift';
|
||||||
import { Project } from 'gql-client';
|
import { Project } from 'gql-client';
|
||||||
import { useDebounceValue } from 'usehooks-ts';
|
import { useDebounce } from 'usehooks-ts';
|
||||||
|
|
||||||
import SearchBar from 'components/SearchBar';
|
import SearchBar from 'components/SearchBar';
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
@ -42,7 +42,7 @@ export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300);
|
const debouncedInputValue = useDebounce<string>(inputValue, 300);
|
||||||
|
|
||||||
const fetchProjects = useCallback(
|
const fetchProjects = useCallback(
|
||||||
async (inputValue: string) => {
|
async (inputValue: string) => {
|
||||||
@ -62,7 +62,7 @@ export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
|
|||||||
<div className="relative w-full lg:w-fit">
|
<div className="relative w-full lg:w-fit">
|
||||||
<SearchBar {...getInputProps()} />
|
<SearchBar {...getInputProps()} />
|
||||||
<div
|
<div
|
||||||
{...getMenuProps({}, { suppressRefError: true })}
|
{...getMenuProps()}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col shadow-dropdown rounded-xl bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
|
'flex flex-col shadow-dropdown rounded-xl bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
|
||||||
{ hidden: !inputValue || !isOpen },
|
{ hidden: !inputValue || !isOpen },
|
||||||
|
@ -5,7 +5,7 @@ import { CrossIcon, SearchIcon } from 'components/shared/CustomIcon';
|
|||||||
import { Input } from 'components/shared/Input';
|
import { Input } from 'components/shared/Input';
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
import { Project } from 'gql-client';
|
import { Project } from 'gql-client';
|
||||||
import { useDebounceValue } from 'usehooks-ts';
|
import { useDebounce } from 'usehooks-ts';
|
||||||
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
|
import { ProjectSearchBarItem } from './ProjectSearchBarItem';
|
||||||
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
|
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@ -27,30 +27,25 @@ export const ProjectSearchBarDialog = ({
|
|||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const {
|
const { getInputProps, getItemProps, inputValue, setInputValue } =
|
||||||
getInputProps,
|
useCombobox({
|
||||||
getItemProps,
|
items,
|
||||||
getMenuProps,
|
itemToString(item) {
|
||||||
inputValue,
|
return item ? item.name : '';
|
||||||
setInputValue,
|
},
|
||||||
} = useCombobox({
|
selectedItem,
|
||||||
items,
|
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
|
||||||
itemToString(item) {
|
if (newSelectedItem) {
|
||||||
return item ? item.name : '';
|
setSelectedItem(newSelectedItem);
|
||||||
},
|
onClickItem?.(newSelectedItem);
|
||||||
selectedItem,
|
navigate(
|
||||||
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
|
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`,
|
||||||
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(
|
const fetchProjects = useCallback(
|
||||||
async (inputValue: string) => {
|
async (inputValue: string) => {
|
||||||
@ -80,7 +75,7 @@ export const ProjectSearchBarDialog = ({
|
|||||||
<div className="h-full flex flex-col fixed top-0 inset-0">
|
<div className="h-full flex flex-col fixed top-0 inset-0">
|
||||||
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
|
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
|
||||||
<Input
|
<Input
|
||||||
{...getInputProps({}, { suppressRefError: true })}
|
{...getInputProps()}
|
||||||
leftIcon={<SearchIcon />}
|
leftIcon={<SearchIcon />}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
appearance="borderless"
|
appearance="borderless"
|
||||||
@ -91,33 +86,23 @@ export const ProjectSearchBarDialog = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div
|
<div className="flex flex-col gap-1 px-2 py-2">
|
||||||
className="flex flex-col gap-1 px-2 py-2"
|
{items.length > 0
|
||||||
{...getMenuProps(
|
? items.map((item, index) => (
|
||||||
{},
|
<>
|
||||||
{
|
<div className="px-2 py-2">
|
||||||
suppressRefError: true,
|
<p className="text-elements-mid-em text-xs font-medium">
|
||||||
},
|
Suggestions
|
||||||
)}
|
</p>
|
||||||
>
|
</div>
|
||||||
{items.length > 0 ? (
|
<ProjectSearchBarItem
|
||||||
<>
|
key={item.id}
|
||||||
<div className="px-2 py-2">
|
item={item}
|
||||||
<p className="text-elements-mid-em text-xs font-medium">
|
{...getItemProps({ item, index })}
|
||||||
Suggestions
|
/>
|
||||||
</p>
|
</>
|
||||||
</div>
|
))
|
||||||
{items.map((item, index) => (
|
: inputValue && <ProjectSearchBarEmpty />}
|
||||||
<ProjectSearchBarItem
|
|
||||||
key={item.id}
|
|
||||||
item={item}
|
|
||||||
{...getItemProps({ item, index })}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
inputValue && <ProjectSearchBarEmpty />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
|
@ -1,571 +0,0 @@
|
|||||||
import { useCallback, useState, useEffect } from 'react';
|
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
|
||||||
import { FormProvider, FieldValues } from 'react-hook-form';
|
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
||||||
import { useMediaQuery } from 'usehooks-ts';
|
|
||||||
import {
|
|
||||||
AddEnvironmentVariableInput,
|
|
||||||
AuctionParams,
|
|
||||||
Deployer,
|
|
||||||
} from 'gql-client';
|
|
||||||
|
|
||||||
import { Select, MenuItem, FormControl, FormHelperText } from '@mui/material';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ArrowRightCircleFilledIcon,
|
|
||||||
LoadingIcon,
|
|
||||||
} from 'components/shared/CustomIcon';
|
|
||||||
import { Heading } from '../../shared/Heading';
|
|
||||||
import { Button } from '../../shared/Button';
|
|
||||||
import { Input } from 'components/shared/Input';
|
|
||||||
import { useToast } from 'components/shared/Toast';
|
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
|
||||||
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
|
|
||||||
import { EnvironmentVariablesFormValues } from 'types/types';
|
|
||||||
import ConnectWallet from './ConnectWallet';
|
|
||||||
import { useWalletConnectClient } from 'context/WalletConnectContext';
|
|
||||||
|
|
||||||
type ConfigureDeploymentFormValues = {
|
|
||||||
option: string;
|
|
||||||
lrn?: string;
|
|
||||||
numProviders?: number;
|
|
||||||
maxPrice?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ConfigureFormValues = ConfigureDeploymentFormValues &
|
|
||||||
EnvironmentVariablesFormValues;
|
|
||||||
|
|
||||||
const DEFAULT_MAX_PRICE = '10000';
|
|
||||||
|
|
||||||
const Configure = () => {
|
|
||||||
const { signClient, session, accounts } = useWalletConnectClient();
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [deployers, setDeployers] = useState<Deployer[]>([]);
|
|
||||||
const [selectedAccount, setSelectedAccount] = useState<string>();
|
|
||||||
const [selectedDeployer, setSelectedDeployer] = useState<Deployer>();
|
|
||||||
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
|
|
||||||
const [isPaymentDone, setIsPaymentDone] = useState(false);
|
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const templateId = searchParams.get('templateId');
|
|
||||||
const queryParams = new URLSearchParams(location.search);
|
|
||||||
|
|
||||||
const owner = queryParams.get('owner');
|
|
||||||
const name = queryParams.get('name');
|
|
||||||
const defaultBranch = queryParams.get('defaultBranch');
|
|
||||||
const fullName = queryParams.get('fullName');
|
|
||||||
const orgSlug = queryParams.get('orgSlug');
|
|
||||||
const templateOwner = queryParams.get('templateOwner');
|
|
||||||
const templateRepo = queryParams.get('templateRepo');
|
|
||||||
const isPrivate = queryParams.get('isPrivate') === 'true';
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { toast, dismiss } = useToast();
|
|
||||||
const client = useGQLClient();
|
|
||||||
|
|
||||||
const methods = useForm<ConfigureFormValues>({
|
|
||||||
defaultValues: {
|
|
||||||
option: 'Auction',
|
|
||||||
maxPrice: DEFAULT_MAX_PRICE,
|
|
||||||
lrn: '',
|
|
||||||
numProviders: 1,
|
|
||||||
variables: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedOption = methods.watch('option');
|
|
||||||
|
|
||||||
const isTabletView = useMediaQuery('(min-width: 720px)'); // md:
|
|
||||||
const buttonSize = isTabletView ? { size: 'lg' as const } : {};
|
|
||||||
|
|
||||||
const createProject = async (
|
|
||||||
data: FieldValues,
|
|
||||||
envVariables: AddEnvironmentVariableInput[],
|
|
||||||
senderAddress: string,
|
|
||||||
txHash: string,
|
|
||||||
): Promise<string> => {
|
|
||||||
setIsLoading(true);
|
|
||||||
let projectId: string | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let lrn: string | undefined;
|
|
||||||
let auctionParams: AuctionParams | undefined;
|
|
||||||
|
|
||||||
if (data.option === 'LRN') {
|
|
||||||
lrn = data.lrn;
|
|
||||||
} else if (data.option === 'Auction') {
|
|
||||||
auctionParams = {
|
|
||||||
numProviders: Number(data.numProviders!),
|
|
||||||
maxPrice: data.maxPrice!.toString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (templateId) {
|
|
||||||
const projectData: any = {
|
|
||||||
templateOwner,
|
|
||||||
templateRepo,
|
|
||||||
owner,
|
|
||||||
name,
|
|
||||||
isPrivate,
|
|
||||||
paymentAddress: senderAddress,
|
|
||||||
txHash,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { addProjectFromTemplate } = await client.addProjectFromTemplate(
|
|
||||||
orgSlug!,
|
|
||||||
projectData,
|
|
||||||
lrn,
|
|
||||||
auctionParams,
|
|
||||||
envVariables,
|
|
||||||
);
|
|
||||||
|
|
||||||
projectId = addProjectFromTemplate.id;
|
|
||||||
} else {
|
|
||||||
const { addProject } = await client.addProject(
|
|
||||||
orgSlug!,
|
|
||||||
{
|
|
||||||
name: `${owner}-${name}`,
|
|
||||||
prodBranch: defaultBranch!,
|
|
||||||
repository: fullName!,
|
|
||||||
template: 'webapp',
|
|
||||||
paymentAddress: senderAddress,
|
|
||||||
txHash,
|
|
||||||
},
|
|
||||||
lrn,
|
|
||||||
auctionParams,
|
|
||||||
envVariables,
|
|
||||||
);
|
|
||||||
|
|
||||||
projectId = addProject.id;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating project:', error);
|
|
||||||
toast({
|
|
||||||
id: 'error-creating-project',
|
|
||||||
title: 'Error creating project',
|
|
||||||
variant: 'error',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projectId) {
|
|
||||||
return projectId;
|
|
||||||
} else {
|
|
||||||
throw new Error('Project creation failed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const verifyTx = async (
|
|
||||||
senderAddress: string,
|
|
||||||
txHash: string,
|
|
||||||
amount: string,
|
|
||||||
): Promise<boolean> => {
|
|
||||||
const isValid = await client.verifyTx(
|
|
||||||
txHash,
|
|
||||||
`${amount.toString()}alnt`,
|
|
||||||
senderAddress,
|
|
||||||
);
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFormSubmit = useCallback(
|
|
||||||
async (createFormData: FieldValues) => {
|
|
||||||
try {
|
|
||||||
const deployerLrn = createFormData.lrn;
|
|
||||||
const deployer = deployers.find(
|
|
||||||
(deployer) => deployer.deployerLrn === deployerLrn,
|
|
||||||
);
|
|
||||||
|
|
||||||
let amount: string;
|
|
||||||
let senderAddress: string;
|
|
||||||
let txHash: string;
|
|
||||||
if (createFormData.option === 'LRN' && !deployer?.minimumPayment) {
|
|
||||||
toast({
|
|
||||||
id: 'no-payment-required',
|
|
||||||
title: 'No payment required. Deploying app...',
|
|
||||||
variant: 'info',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
|
|
||||||
txHash = '';
|
|
||||||
senderAddress = '';
|
|
||||||
} else {
|
|
||||||
if (!selectedAccount) return;
|
|
||||||
|
|
||||||
senderAddress = selectedAccount.split(':')[2];
|
|
||||||
|
|
||||||
if (createFormData.option === 'LRN') {
|
|
||||||
amount = deployer?.minimumPayment!;
|
|
||||||
} else {
|
|
||||||
amount = (
|
|
||||||
createFormData.numProviders * createFormData.maxPrice
|
|
||||||
).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const amountToBePaid = amount.replace(/\D/g, '').toString();
|
|
||||||
|
|
||||||
const txHashResponse = await cosmosSendTokensHandler(
|
|
||||||
selectedAccount,
|
|
||||||
amountToBePaid,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!txHashResponse) {
|
|
||||||
console.error('Tx not successful');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
txHash = txHashResponse;
|
|
||||||
|
|
||||||
const isTxHashValid = await verifyTx(
|
|
||||||
senderAddress,
|
|
||||||
txHash,
|
|
||||||
amountToBePaid.toString(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isTxHashValid === false) {
|
|
||||||
console.error('Invalid Tx hash', txHash);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const environmentVariables = createFormData.variables.map(
|
|
||||||
(variable: any) => {
|
|
||||||
return {
|
|
||||||
key: variable.key,
|
|
||||||
value: variable.value,
|
|
||||||
environments: Object.entries(createFormData.environment)
|
|
||||||
.filter(([, value]) => value === true)
|
|
||||||
.map(([key]) => key.charAt(0).toUpperCase() + key.slice(1)),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectId = await createProject(
|
|
||||||
createFormData,
|
|
||||||
environmentVariables,
|
|
||||||
senderAddress,
|
|
||||||
txHash,
|
|
||||||
);
|
|
||||||
|
|
||||||
await client.getEnvironmentVariables(projectId);
|
|
||||||
|
|
||||||
if (templateId) {
|
|
||||||
createFormData.option === 'Auction'
|
|
||||||
? navigate(
|
|
||||||
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
|
|
||||||
)
|
|
||||||
: navigate(
|
|
||||||
`/${orgSlug}/projects/create/template/deploy?projectId=${projectId}&templateId=${templateId}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
createFormData.option === 'Auction'
|
|
||||||
? navigate(
|
|
||||||
`/${orgSlug}/projects/create/success/${projectId}?isAuction=true`,
|
|
||||||
)
|
|
||||||
: navigate(
|
|
||||||
`/${orgSlug}/projects/create/deploy?projectId=${projectId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
toast({
|
|
||||||
id: 'error-deploying-app',
|
|
||||||
title: 'Error deploying app',
|
|
||||||
variant: 'error',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[client, createProject, dismiss, toast],
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchDeployers = useCallback(async () => {
|
|
||||||
const res = await client.getDeployers();
|
|
||||||
setDeployers(res.deployers);
|
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
const onAccountChange = useCallback((account: string) => {
|
|
||||||
setSelectedAccount(account);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onDeployerChange = useCallback(
|
|
||||||
(selectedLrn: string) => {
|
|
||||||
const deployer = deployers.find((d) => d.deployerLrn === selectedLrn);
|
|
||||||
setSelectedDeployer(deployer);
|
|
||||||
},
|
|
||||||
[deployers],
|
|
||||||
);
|
|
||||||
|
|
||||||
const cosmosSendTokensHandler = useCallback(
|
|
||||||
async (selectedAccount: string, amount: string) => {
|
|
||||||
if (!signClient || !session || !selectedAccount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chainId = selectedAccount.split(':')[1];
|
|
||||||
const senderAddress = selectedAccount.split(':')[2];
|
|
||||||
const snowballAddress = await client.getAddress();
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsPaymentDone(false);
|
|
||||||
setIsPaymentLoading(true);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
id: 'sending-payment-request',
|
|
||||||
title: 'Check your wallet and approve payment request',
|
|
||||||
variant: 'loading',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result: { signature: string } = await signClient.request({
|
|
||||||
topic: session.topic,
|
|
||||||
chainId: `cosmos:${chainId}`,
|
|
||||||
request: {
|
|
||||||
method: 'cosmos_sendTokens',
|
|
||||||
params: [
|
|
||||||
{
|
|
||||||
from: senderAddress,
|
|
||||||
to: snowballAddress,
|
|
||||||
value: amount,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
throw new Error('Error completing transaction');
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
id: 'payment-successful',
|
|
||||||
title: 'Payment successful',
|
|
||||||
variant: 'success',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsPaymentDone(true);
|
|
||||||
|
|
||||||
return result.signature;
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error sending tokens', error);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
id: 'error-sending-tokens',
|
|
||||||
title: 'Error sending tokens',
|
|
||||||
variant: 'error',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsPaymentDone(false);
|
|
||||||
} finally {
|
|
||||||
setIsPaymentLoading(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[session, signClient, toast],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDeployers();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-7 px-4 py-6">
|
|
||||||
<div className="flex justify-between mb-6">
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Heading as="h4" className="md:text-lg font-medium">
|
|
||||||
Configure deployment
|
|
||||||
</Heading>
|
|
||||||
<Heading as="h5" className="text-sm font-sans text-elements-low-em">
|
|
||||||
The app can be deployed by setting the deployer LRN for a single
|
|
||||||
deployment or by creating a deployer auction for multiple
|
|
||||||
deployments
|
|
||||||
</Heading>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-6 lg:gap-8 w-full">
|
|
||||||
<FormProvider {...methods}>
|
|
||||||
<form onSubmit={methods.handleSubmit(handleFormSubmit)}>
|
|
||||||
<div className="flex flex-col justify-start gap-4 mb-6">
|
|
||||||
<Controller
|
|
||||||
name="option"
|
|
||||||
control={methods.control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<Select
|
|
||||||
value={value}
|
|
||||||
onChange={(event) => onChange(event.target.value)}
|
|
||||||
size="small"
|
|
||||||
displayEmpty
|
|
||||||
sx={{
|
|
||||||
fontFamily: 'inherit',
|
|
||||||
'& .MuiOutlinedInput-notchedOutline': {
|
|
||||||
borderColor: '#e0e0e0',
|
|
||||||
borderRadius: '8px',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItem value="Auction">Create Auction</MenuItem>
|
|
||||||
<MenuItem value="LRN">Deployer LRN</MenuItem>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedOption === 'LRN' && (
|
|
||||||
<div className="flex flex-col justify-start gap-4 mb-6">
|
|
||||||
<Heading
|
|
||||||
as="h5"
|
|
||||||
className="text-sm font-sans text-elements-low-em"
|
|
||||||
>
|
|
||||||
The app will be deployed by the configured deployer
|
|
||||||
</Heading>
|
|
||||||
<Controller
|
|
||||||
name="lrn"
|
|
||||||
control={methods.control}
|
|
||||||
rules={{ required: true }}
|
|
||||||
render={({ field: { value, onChange }, fieldState }) => (
|
|
||||||
<FormControl fullWidth error={Boolean(fieldState.error)}>
|
|
||||||
<span className="text-sm text-elements-high-em mb-4">
|
|
||||||
Select deployer LRN
|
|
||||||
</span>
|
|
||||||
<Select
|
|
||||||
value={value}
|
|
||||||
onChange={(event) => {
|
|
||||||
onChange(event.target.value);
|
|
||||||
onDeployerChange(event.target.value);
|
|
||||||
}}
|
|
||||||
displayEmpty
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{deployers.map((deployer) => (
|
|
||||||
<MenuItem
|
|
||||||
key={deployer.deployerLrn}
|
|
||||||
value={deployer.deployerLrn}
|
|
||||||
>
|
|
||||||
{`${deployer.deployerLrn} ${deployer.minimumPayment ? `(${deployer.minimumPayment})` : ''}`}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
{fieldState.error && (
|
|
||||||
<FormHelperText>
|
|
||||||
{fieldState.error.message}
|
|
||||||
</FormHelperText>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedOption === 'Auction' && (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col justify-start gap-4 mb-6">
|
|
||||||
<Heading
|
|
||||||
as="h5"
|
|
||||||
className="text-sm font-sans text-elements-low-em"
|
|
||||||
>
|
|
||||||
Set the number of deployers and maximum price for each
|
|
||||||
deployment
|
|
||||||
</Heading>
|
|
||||||
<span className="text-sm text-elements-high-em">
|
|
||||||
Number of Deployers
|
|
||||||
</span>
|
|
||||||
<Controller
|
|
||||||
name="numProviders"
|
|
||||||
control={methods.control}
|
|
||||||
rules={{ required: true }}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-start gap-4 mb-6">
|
|
||||||
<span className="text-sm text-elements-high-em">
|
|
||||||
Maximum Price (alnt)
|
|
||||||
</span>
|
|
||||||
<Controller
|
|
||||||
name="maxPrice"
|
|
||||||
control={methods.control}
|
|
||||||
rules={{ required: true }}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<Input type="number" value={value} onChange={onChange} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Heading as="h4" className="md:text-lg font-medium mb-3">
|
|
||||||
Environment Variables
|
|
||||||
</Heading>
|
|
||||||
<div className="p-4 bg-slate-100 rounded-lg mb-6">
|
|
||||||
<EnvironmentVariablesForm />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedOption === 'LRN' && !selectedDeployer?.minimumPayment ? (
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
{...buttonSize}
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading || !selectedDeployer || !selectedAccount}
|
|
||||||
rightIcon={
|
|
||||||
isLoading ? (
|
|
||||||
<LoadingIcon className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<ArrowRightCircleFilledIcon />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Deploying' : 'Deploy'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Heading as="h4" className="md:text-lg font-medium mb-3">
|
|
||||||
Connect to your wallet
|
|
||||||
</Heading>
|
|
||||||
<ConnectWallet onAccountChange={onAccountChange} />
|
|
||||||
{accounts && accounts?.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
{...buttonSize}
|
|
||||||
type="submit"
|
|
||||||
disabled={
|
|
||||||
isLoading || isPaymentLoading || !selectedAccount
|
|
||||||
}
|
|
||||||
rightIcon={
|
|
||||||
isLoading || isPaymentLoading ? (
|
|
||||||
<LoadingIcon className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<ArrowRightCircleFilledIcon />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!isPaymentDone
|
|
||||||
? isPaymentLoading
|
|
||||||
? 'Transaction Requested'
|
|
||||||
: 'Pay and Deploy'
|
|
||||||
: isLoading
|
|
||||||
? 'Deploying'
|
|
||||||
: 'Deploy'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</FormProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Configure;
|
|
@ -1,46 +0,0 @@
|
|||||||
import { Select, Option } from '@snowballtools/material-tailwind-react-fork';
|
|
||||||
|
|
||||||
import { Button } from '../../shared/Button';
|
|
||||||
import { useWalletConnectClient } from 'context/WalletConnectContext';
|
|
||||||
|
|
||||||
const ConnectWallet = ({
|
|
||||||
onAccountChange,
|
|
||||||
}: {
|
|
||||||
onAccountChange: (selectedAccount: string) => void;
|
|
||||||
}) => {
|
|
||||||
const { onConnect, accounts } = useWalletConnectClient();
|
|
||||||
|
|
||||||
const handleConnect = async () => {
|
|
||||||
await onConnect();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4 bg-slate-100 rounded-lg mb-6">
|
|
||||||
{!accounts ? (
|
|
||||||
<div>
|
|
||||||
<Button type={'button'} onClick={handleConnect}>
|
|
||||||
Connect Wallet
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<Select
|
|
||||||
label="Select Account"
|
|
||||||
defaultValue={accounts[0].address}
|
|
||||||
onChange={(value) => {
|
|
||||||
value && onAccountChange(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{accounts.map((account, index) => (
|
|
||||||
<Option key={index} value={account.address}>
|
|
||||||
{account.address.split(':').slice(1).join(':')}
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConnectWallet;
|
|
@ -1,7 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import axios from 'axios';
|
|
||||||
import { Deployment } from 'gql-client';
|
|
||||||
|
|
||||||
import { DeployStep, DeployStatus } from './DeployStep';
|
import { DeployStep, DeployStatus } from './DeployStep';
|
||||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||||
@ -9,37 +7,13 @@ import { Heading } from '../../shared/Heading';
|
|||||||
import { Button } from '../../shared/Button';
|
import { Button } from '../../shared/Button';
|
||||||
import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon';
|
import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon';
|
||||||
import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog';
|
import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog';
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
|
||||||
|
|
||||||
const FETCH_DEPLOYMENTS_INTERVAL = 5000;
|
|
||||||
|
|
||||||
type RequestState =
|
|
||||||
| 'SUBMITTED'
|
|
||||||
| 'DEPLOYING'
|
|
||||||
| 'DEPLOYED'
|
|
||||||
| 'REMOVED'
|
|
||||||
| 'CANCELLED'
|
|
||||||
| 'ERROR';
|
|
||||||
|
|
||||||
type Record = {
|
|
||||||
id: string;
|
|
||||||
createTime: string;
|
|
||||||
app: string;
|
|
||||||
lastState: RequestState;
|
|
||||||
lastUpdate: string;
|
|
||||||
logAvailable: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const TIMEOUT_DURATION = 5000;
|
||||||
const Deploy = () => {
|
const Deploy = () => {
|
||||||
const client = useGQLClient();
|
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const projectId = searchParams.get('projectId');
|
const projectId = searchParams.get('projectId');
|
||||||
|
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [deployment, setDeployment] = useState<Deployment>();
|
|
||||||
const [record, setRecord] = useState<Record>();
|
|
||||||
|
|
||||||
const handleOpen = () => setOpen(!open);
|
const handleOpen = () => setOpen(!open);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -49,67 +23,13 @@ const Deploy = () => {
|
|||||||
navigate(`/${orgSlug}/projects/create`);
|
navigate(`/${orgSlug}/projects/create`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isDeploymentFailed = useMemo(() => {
|
|
||||||
if (!record) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not checking for `REMOVED` status as this status is received for a brief period before receiving `DEPLOYED` status
|
|
||||||
if (record.lastState === 'CANCELLED' || record.lastState === 'ERROR') {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [record]);
|
|
||||||
|
|
||||||
const fetchDeploymentRecords = useCallback(async () => {
|
|
||||||
if (!deployment) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const record: Record = response.data;
|
|
||||||
setRecord(record);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.log('Error fetching data from deployer', err);
|
|
||||||
}
|
|
||||||
}, [deployment]);
|
|
||||||
|
|
||||||
const fetchDeployment = useCallback(async () => {
|
|
||||||
if (!projectId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { deployments } = await client.getDeployments(projectId);
|
|
||||||
setDeployment(deployments[0]);
|
|
||||||
}, [client, projectId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDeployment();
|
const timerID = setTimeout(() => {
|
||||||
fetchDeploymentRecords();
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
fetchDeploymentRecords();
|
|
||||||
}, FETCH_DEPLOYMENTS_INTERVAL);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
}, [fetchDeployment, fetchDeploymentRecords]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!record) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record.lastState === 'DEPLOYED') {
|
|
||||||
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
||||||
}
|
}, TIMEOUT_DURATION);
|
||||||
}, [record]);
|
|
||||||
|
return () => clearInterval(timerID);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-7">
|
<div className="space-y-7">
|
||||||
@ -122,7 +42,6 @@ const Deploy = () => {
|
|||||||
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
|
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
|
||||||
<Stopwatch
|
<Stopwatch
|
||||||
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
||||||
isPaused={isDeploymentFailed}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -141,36 +60,30 @@ const Deploy = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isDeploymentFailed ? (
|
<div>
|
||||||
<div>
|
<DeployStep
|
||||||
<DeployStep
|
title="Building"
|
||||||
title={record ? 'Submitted' : 'Submitting'}
|
status={DeployStatus.COMPLETE}
|
||||||
status={record ? DeployStatus.COMPLETE : DeployStatus.PROCESSING}
|
step="1"
|
||||||
step="1"
|
processTime="72000"
|
||||||
/>
|
/>
|
||||||
|
<DeployStep
|
||||||
<DeployStep
|
title="Deployment summary"
|
||||||
title={
|
status={DeployStatus.PROCESSING}
|
||||||
record && record.lastState === 'DEPLOYED'
|
step="2"
|
||||||
? 'Deployed'
|
startTime={Date.now().toString()}
|
||||||
: 'Deploying'
|
/>
|
||||||
}
|
<DeployStep
|
||||||
status={
|
title="Running checks"
|
||||||
!record
|
status={DeployStatus.NOT_STARTED}
|
||||||
? DeployStatus.NOT_STARTED
|
step="3"
|
||||||
: record.lastState === 'DEPLOYED'
|
/>
|
||||||
? DeployStatus.COMPLETE
|
<DeployStep
|
||||||
: DeployStatus.PROCESSING
|
title="Assigning domains"
|
||||||
}
|
status={DeployStatus.NOT_STARTED}
|
||||||
step="2"
|
step="4"
|
||||||
startTime={Date.now().toString()}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<DeployStep title={record!.lastState} status={DeployStatus.ERROR} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,16 +1,27 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
|
||||||
|
|
||||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||||
|
import FormatMillisecond from '../../FormatMilliSecond';
|
||||||
|
import processLogs from '../../../assets/process-logs.json';
|
||||||
import { cn } from 'utils/classnames';
|
import { cn } from 'utils/classnames';
|
||||||
import {
|
import {
|
||||||
CheckRoundFilledIcon,
|
CheckRoundFilledIcon,
|
||||||
ClockOutlineIcon,
|
ClockOutlineIcon,
|
||||||
|
CopyIcon,
|
||||||
LoaderIcon,
|
LoaderIcon,
|
||||||
|
MinusCircleIcon,
|
||||||
|
PlusIcon,
|
||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
import { useIntersectionObserver } from 'usehooks-ts';
|
||||||
|
|
||||||
enum DeployStatus {
|
enum DeployStatus {
|
||||||
PROCESSING = 'progress',
|
PROCESSING = 'progress',
|
||||||
COMPLETE = 'complete',
|
COMPLETE = 'complete',
|
||||||
NOT_STARTED = 'notStarted',
|
NOT_STARTED = 'notStarted',
|
||||||
ERROR = 'error',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeployStepsProps {
|
interface DeployStepsProps {
|
||||||
@ -21,11 +32,35 @@ interface DeployStepsProps {
|
|||||||
processTime?: string;
|
processTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
|
const DeployStep = ({
|
||||||
|
step,
|
||||||
|
status,
|
||||||
|
title,
|
||||||
|
startTime,
|
||||||
|
processTime,
|
||||||
|
}: DeployStepsProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { toast, dismiss } = useToast();
|
||||||
|
const { isIntersecting: hideGradientOverlay, ref } = useIntersectionObserver({
|
||||||
|
threshold: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const disableCollapse = status !== DeployStatus.COMPLETE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border-separator">
|
<div className="border-b border-border-separator">
|
||||||
|
{/* Collapisble trigger */}
|
||||||
<button
|
<button
|
||||||
className={cn('flex justify-between w-full py-5 gap-2', 'cursor-auto')}
|
className={cn(
|
||||||
|
'flex justify-between w-full py-5 gap-2',
|
||||||
|
disableCollapse && 'cursor-auto',
|
||||||
|
)}
|
||||||
|
tabIndex={disableCollapse ? -1 : undefined}
|
||||||
|
onClick={() => {
|
||||||
|
if (!disableCollapse) {
|
||||||
|
setIsOpen((val) => !val);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className={cn('grow flex items-center gap-3')}>
|
<div className={cn('grow flex items-center gap-3')}>
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
@ -38,6 +73,12 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
|
|||||||
{status === DeployStatus.PROCESSING && (
|
{status === DeployStatus.PROCESSING && (
|
||||||
<LoaderIcon className="animate-spin text-elements-link" />
|
<LoaderIcon className="animate-spin text-elements-link" />
|
||||||
)}
|
)}
|
||||||
|
{status === DeployStatus.COMPLETE && (
|
||||||
|
<div className="text-controls-primary">
|
||||||
|
{!isOpen && <PlusIcon size={24} />}
|
||||||
|
{isOpen && <MinusCircleIcon size={24} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
@ -55,10 +96,7 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
|
|||||||
{status === DeployStatus.PROCESSING && (
|
{status === DeployStatus.PROCESSING && (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<ClockOutlineIcon size={16} className="text-elements-low-em" />
|
<ClockOutlineIcon size={16} className="text-elements-low-em" />
|
||||||
<Stopwatch
|
<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
||||||
offsetTimestamp={setStopWatchOffset(startTime!)}
|
|
||||||
isPaused={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{status === DeployStatus.COMPLETE && (
|
{status === DeployStatus.COMPLETE && (
|
||||||
@ -69,9 +107,51 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
|
|||||||
size={15}
|
size={15}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<FormatMillisecond time={Number(processTime)} />{' '}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Collapsible */}
|
||||||
|
<Collapse open={isOpen}>
|
||||||
|
<div className="relative text-xs text-elements-low-em h-36 overflow-y-auto">
|
||||||
|
{/* Logs */}
|
||||||
|
{processLogs.map((log, key) => {
|
||||||
|
return (
|
||||||
|
<p className="font-mono" key={key}>
|
||||||
|
{log}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* End of logs ref used for hiding gradient overlay */}
|
||||||
|
<div ref={ref} />
|
||||||
|
|
||||||
|
{/* Overflow gradient overlay */}
|
||||||
|
{!hideGradientOverlay && (
|
||||||
|
<div className="h-14 w-full sticky bottom-0 inset-x-0 bg-gradient-to-t from-white to-transparent" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Copy log button */}
|
||||||
|
<div className={cn('sticky bottom-4 left-1/2 flex justify-center')}>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(processLogs.join('\n'));
|
||||||
|
toast({
|
||||||
|
title: 'Logs copied',
|
||||||
|
variant: 'success',
|
||||||
|
id: 'logs',
|
||||||
|
onDismiss: dismiss,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
leftIcon={<CopyIcon size={16} />}
|
||||||
|
>
|
||||||
|
Copy log
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -38,9 +38,36 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(
|
try {
|
||||||
`configure?owner=${repository.owner?.login}&name=${repository.name}&defaultBranch=${repository.default_branch}&fullName=${repository.full_name}&orgSlug=${orgSlug}`,
|
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]);
|
}, [client, repository, orgSlug, setIsLoading, navigate, toast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -64,9 +64,9 @@ export const RepositoryList = () => {
|
|||||||
|
|
||||||
// Check if selected account is an organization
|
// Check if selected account is an organization
|
||||||
if (selectedAccount.value === gitUser.login) {
|
if (selectedAccount.value === gitUser.login) {
|
||||||
query = query + ` user:${selectedAccount.value}`;
|
query = query + ` user:${selectedAccount}`;
|
||||||
} else {
|
} else {
|
||||||
query = query + ` org:${selectedAccount.value}`;
|
query = query + ` org:${selectedAccount}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await octokit.rest.search.repos({
|
const result = await octokit.rest.search.repos({
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Deployment,
|
Deployment,
|
||||||
DeploymentStatus,
|
DeploymentStatus,
|
||||||
@ -6,15 +6,6 @@ import {
|
|||||||
Environment,
|
Environment,
|
||||||
Project,
|
Project,
|
||||||
} from 'gql-client';
|
} from 'gql-client';
|
||||||
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
Tooltip,
|
|
||||||
} from '@mui/material';
|
|
||||||
|
|
||||||
import { Avatar } from 'components/shared/Avatar';
|
import { Avatar } from 'components/shared/Avatar';
|
||||||
import {
|
import {
|
||||||
BranchStrokeIcon,
|
BranchStrokeIcon,
|
||||||
@ -27,23 +18,12 @@ import {
|
|||||||
import { Heading } from 'components/shared/Heading';
|
import { Heading } from 'components/shared/Heading';
|
||||||
import { OverflownText } from 'components/shared/OverflownText';
|
import { OverflownText } from 'components/shared/OverflownText';
|
||||||
import { Tag, TagTheme } from 'components/shared/Tag';
|
import { Tag, TagTheme } from 'components/shared/Tag';
|
||||||
import { Button } from 'components/shared/Button';
|
|
||||||
import { getInitials } from 'utils/geInitials';
|
import { getInitials } from 'utils/geInitials';
|
||||||
import { relativeTimeMs } from 'utils/time';
|
import { relativeTimeMs } from 'utils/time';
|
||||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||||
import { formatAddress } from '../../../../utils/format';
|
import { formatAddress } from '../../../../utils/format';
|
||||||
import { DeploymentMenu } from './DeploymentMenu';
|
import { DeploymentMenu } from './DeploymentMenu';
|
||||||
|
|
||||||
const DEPLOYMENT_LOGS_STYLE = {
|
|
||||||
backgroundColor: 'rgba(0,0,0, .9)',
|
|
||||||
padding: '2em',
|
|
||||||
borderRadius: '0.5em',
|
|
||||||
marginLeft: '0.5em',
|
|
||||||
marginRight: '0.5em',
|
|
||||||
color: 'gray',
|
|
||||||
fontSize: 'small',
|
|
||||||
};
|
|
||||||
|
|
||||||
interface DeployDetailsCardProps {
|
interface DeployDetailsCardProps {
|
||||||
deployment: Deployment;
|
deployment: Deployment;
|
||||||
currentDeployment: Deployment;
|
currentDeployment: Deployment;
|
||||||
@ -68,14 +48,6 @@ const DeploymentDetailsCard = ({
|
|||||||
project,
|
project,
|
||||||
prodBranchDomains,
|
prodBranchDomains,
|
||||||
}: DeployDetailsCardProps) => {
|
}: DeployDetailsCardProps) => {
|
||||||
const [openDialog, setOpenDialog] = useState<boolean>(false);
|
|
||||||
const [deploymentLogs, setDeploymentLogs] = useState<string>(
|
|
||||||
'No deployment logs available',
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOpenDialog = () => setOpenDialog(true);
|
|
||||||
const handleCloseDialog = () => setOpenDialog(false);
|
|
||||||
|
|
||||||
const getIconByDeploymentStatus = (status: DeploymentStatus) => {
|
const getIconByDeploymentStatus = (status: DeploymentStatus) => {
|
||||||
if (
|
if (
|
||||||
status === DeploymentStatus.Building ||
|
status === DeploymentStatus.Building ||
|
||||||
@ -92,39 +64,18 @@ const DeploymentDetailsCard = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchDeploymentLogs = async () => {
|
|
||||||
setDeploymentLogs('Loading logs...');
|
|
||||||
handleOpenDialog();
|
|
||||||
const statusUrl = `${deployment.deployer.deployerApiUrl}/${deployment.applicationDeploymentRequestId}`;
|
|
||||||
const statusRes = await fetch(statusUrl, { cache: 'no-store' }).then(
|
|
||||||
(res) => res.json(),
|
|
||||||
);
|
|
||||||
if (!statusRes.logAvailable) {
|
|
||||||
setDeploymentLogs(statusRes.lastState);
|
|
||||||
} else {
|
|
||||||
const logsUrl = `${deployment.deployer.deployerApiUrl}/log/${deployment.applicationDeploymentRequestId}`;
|
|
||||||
const logsRes = await fetch(logsUrl, { cache: 'no-store' }).then((res) =>
|
|
||||||
res.text(),
|
|
||||||
);
|
|
||||||
setDeploymentLogs(logsRes);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderDeploymentStatus = useCallback(
|
const renderDeploymentStatus = useCallback(
|
||||||
(className?: string) => {
|
(className?: string) => {
|
||||||
return (
|
return (
|
||||||
<Tooltip title="Click to view build logs">
|
<div className={className}>
|
||||||
<div className={className} style={{ cursor: 'pointer' }}>
|
<Tag
|
||||||
<Tag
|
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||||
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
size="xs"
|
||||||
size="xs"
|
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||||
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
>
|
||||||
onClick={fetchDeploymentLogs}
|
{deployment.status}
|
||||||
>
|
</Tag>
|
||||||
{deployment.status}
|
</div>
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[deployment.status, deployment.commitHash],
|
[deployment.status, deployment.commitHash],
|
||||||
@ -132,7 +83,7 @@ const DeploymentDetailsCard = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative">
|
<div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative">
|
||||||
<div className="flex-1 flex justify-between w-full md:max-w-[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">
|
<div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full">
|
||||||
{/* DEPLOYMENT URL */}
|
{/* DEPLOYMENT URL */}
|
||||||
{deployment.url && (
|
{deployment.url && (
|
||||||
@ -145,12 +96,7 @@ const DeploymentDetailsCard = ({
|
|||||||
</OverflownText>
|
</OverflownText>
|
||||||
</Heading>
|
</Heading>
|
||||||
)}
|
)}
|
||||||
{deployment.deployer.deployerLrn && (
|
<span className="text-sm text-elements-low-em tracking-tight">
|
||||||
<span className="text-sm text-elements-low-em tracking-tight block mt-2">
|
|
||||||
Deployer LRN: {deployment.deployer.deployerLrn}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="text-sm text-elements-low-em tracking-tight block">
|
|
||||||
{deployment.environment === Environment.Production
|
{deployment.environment === Environment.Production
|
||||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||||
: 'Preview'}
|
: 'Preview'}
|
||||||
@ -216,20 +162,6 @@ const DeploymentDetailsCard = ({
|
|||||||
prodBranchDomains={prodBranchDomains}
|
prodBranchDomains={prodBranchDomains}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Dialog
|
|
||||||
open={openDialog}
|
|
||||||
onClose={handleCloseDialog}
|
|
||||||
fullWidth
|
|
||||||
maxWidth="md"
|
|
||||||
>
|
|
||||||
<DialogTitle>Deployment logs</DialogTitle>
|
|
||||||
<DialogContent style={DEPLOYMENT_LOGS_STYLE}>
|
|
||||||
{deploymentLogs && <pre>{deploymentLogs}</pre>}
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={handleCloseDialog}>Close</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -23,7 +23,6 @@ import { useGQLClient } from 'context/GQLClientContext';
|
|||||||
import { cn } from 'utils/classnames';
|
import { cn } from 'utils/classnames';
|
||||||
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
|
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
|
||||||
import { useToast } from 'components/shared/Toast';
|
import { useToast } from 'components/shared/Toast';
|
||||||
import { DeleteDeploymentDialog } from 'components/projects/Dialog/DeleteDeploymentDialog';
|
|
||||||
|
|
||||||
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
|
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
|
||||||
deployment: Deployment;
|
deployment: Deployment;
|
||||||
@ -47,16 +46,12 @@ export const DeploymentMenu = ({
|
|||||||
|
|
||||||
const [changeToProduction, setChangeToProduction] = useState(false);
|
const [changeToProduction, setChangeToProduction] = useState(false);
|
||||||
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
||||||
const [deleteDeploymentDialog, setDeleteDeploymentDialog] = useState(false);
|
|
||||||
const [isConfirmDeleteLoading, setIsConfirmDeleteLoading] = useState(false);
|
|
||||||
const [rollbackDeployment, setRollbackDeployment] = useState(false);
|
const [rollbackDeployment, setRollbackDeployment] = useState(false);
|
||||||
const [assignDomainDialog, setAssignDomainDialog] = useState(false);
|
const [assignDomainDialog, setAssignDomainDialog] = useState(false);
|
||||||
const [isConfirmButtonLoading, setConfirmButtonLoadingLoading] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
const updateDeployment = async () => {
|
const updateDeployment = async () => {
|
||||||
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
||||||
if (isUpdated.updateDeploymentToProd) {
|
if (isUpdated) {
|
||||||
await onUpdate();
|
await onUpdate();
|
||||||
toast({
|
toast({
|
||||||
id: 'deployment_changed_to_production',
|
id: 'deployment_changed_to_production',
|
||||||
@ -76,8 +71,7 @@ export const DeploymentMenu = ({
|
|||||||
|
|
||||||
const redeployToProd = async () => {
|
const redeployToProd = async () => {
|
||||||
const isRedeployed = await client.redeployToProd(deployment.id);
|
const isRedeployed = await client.redeployToProd(deployment.id);
|
||||||
setConfirmButtonLoadingLoading(false);
|
if (isRedeployed) {
|
||||||
if (isRedeployed.redeployToProd) {
|
|
||||||
await onUpdate();
|
await onUpdate();
|
||||||
toast({
|
toast({
|
||||||
id: 'redeployed_to_production',
|
id: 'redeployed_to_production',
|
||||||
@ -100,7 +94,7 @@ export const DeploymentMenu = ({
|
|||||||
project.id,
|
project.id,
|
||||||
deployment.id,
|
deployment.id,
|
||||||
);
|
);
|
||||||
if (isRollbacked.rollbackDeployment) {
|
if (isRollbacked) {
|
||||||
await onUpdate();
|
await onUpdate();
|
||||||
toast({
|
toast({
|
||||||
id: 'deployment_rolled_back',
|
id: 'deployment_rolled_back',
|
||||||
@ -120,15 +114,11 @@ export const DeploymentMenu = ({
|
|||||||
|
|
||||||
const deleteDeployment = async () => {
|
const deleteDeployment = async () => {
|
||||||
const isDeleted = await client.deleteDeployment(deployment.id);
|
const isDeleted = await client.deleteDeployment(deployment.id);
|
||||||
|
if (isDeleted) {
|
||||||
setIsConfirmDeleteLoading(false);
|
|
||||||
setDeleteDeploymentDialog((preVal) => !preVal);
|
|
||||||
|
|
||||||
if (isDeleted.deleteDeployment) {
|
|
||||||
await onUpdate();
|
await onUpdate();
|
||||||
toast({
|
toast({
|
||||||
id: 'deployment_removal_requested',
|
id: 'deployment_deleted',
|
||||||
title: 'Deployment removal requested',
|
title: 'Deployment deleted',
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
onDismiss: dismiss,
|
onDismiss: dismiss,
|
||||||
});
|
});
|
||||||
@ -212,7 +202,7 @@ export const DeploymentMenu = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||||
onClick={() => setDeleteDeploymentDialog((preVal) => !preVal)}
|
onClick={() => deleteDeployment()}
|
||||||
>
|
>
|
||||||
<CrossCircleIcon /> Delete deployment
|
<CrossCircleIcon /> Delete deployment
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -238,13 +228,11 @@ export const DeploymentMenu = ({
|
|||||||
open={redeployToProduction}
|
open={redeployToProduction}
|
||||||
confirmButtonTitle="Redeploy"
|
confirmButtonTitle="Redeploy"
|
||||||
handleConfirm={async () => {
|
handleConfirm={async () => {
|
||||||
setConfirmButtonLoadingLoading(true);
|
|
||||||
await redeployToProd();
|
await redeployToProd();
|
||||||
setRedeployToProduction((preVal) => !preVal);
|
setRedeployToProduction((preVal) => !preVal);
|
||||||
}}
|
}}
|
||||||
deployment={deployment}
|
deployment={deployment}
|
||||||
domains={deployment.domain ? [deployment.domain] : []}
|
domains={deployment.domain ? [deployment.domain] : []}
|
||||||
isConfirmButtonLoading={isConfirmButtonLoading}
|
|
||||||
/>
|
/>
|
||||||
{Boolean(currentDeployment) && (
|
{Boolean(currentDeployment) && (
|
||||||
<ChangeStateToProductionDialog
|
<ChangeStateToProductionDialog
|
||||||
@ -265,15 +253,6 @@ export const DeploymentMenu = ({
|
|||||||
open={assignDomainDialog}
|
open={assignDomainDialog}
|
||||||
handleOpen={() => setAssignDomainDialog(!assignDomainDialog)}
|
handleOpen={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||||
/>
|
/>
|
||||||
<DeleteDeploymentDialog
|
|
||||||
open={deleteDeploymentDialog}
|
|
||||||
handleConfirm={async () => {
|
|
||||||
setIsConfirmDeleteLoading(true);
|
|
||||||
await deleteDeployment();
|
|
||||||
}}
|
|
||||||
handleCancel={() => setDeleteDeploymentDialog((preVal) => !preVal)}
|
|
||||||
isConfirmButtonLoading={isConfirmDeleteLoading}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,171 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { Auction, Deployer, Project } from 'gql-client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
} from '@mui/material';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CheckRoundFilledIcon,
|
|
||||||
LoadingIcon,
|
|
||||||
} from 'components/shared/CustomIcon';
|
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
|
||||||
import { Button, Heading, Tag } from 'components/shared';
|
|
||||||
|
|
||||||
const WAIT_DURATION = 5000;
|
|
||||||
|
|
||||||
const DIALOG_STYLE = {
|
|
||||||
backgroundColor: 'rgba(0,0,0, .9)',
|
|
||||||
padding: '2em',
|
|
||||||
borderRadius: '0.5em',
|
|
||||||
marginLeft: '0.5em',
|
|
||||||
marginRight: '0.5em',
|
|
||||||
color: 'gray',
|
|
||||||
fontSize: 'small',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AuctionCard = ({ project }: { project: Project }) => {
|
|
||||||
const [auctionStatus, setAuctionStatus] = useState<string>('');
|
|
||||||
const [deployers, setDeployers] = useState<Deployer[]>([]);
|
|
||||||
const [fundsStatus, setFundsStatus] = useState<boolean>(false);
|
|
||||||
const [auctionDetails, setAuctionDetails] = useState<Auction | null>(null);
|
|
||||||
const [openDialog, setOpenDialog] = useState<boolean>(false);
|
|
||||||
const client = useGQLClient();
|
|
||||||
|
|
||||||
const getIconByAuctionStatus = (status: string) =>
|
|
||||||
status === 'completed' ? (
|
|
||||||
<CheckRoundFilledIcon />
|
|
||||||
) : (
|
|
||||||
<LoadingIcon className="animate-spin" />
|
|
||||||
);
|
|
||||||
|
|
||||||
const checkAuctionStatus = useCallback(async () => {
|
|
||||||
const result = await client.getAuctionData(project.auctionId);
|
|
||||||
setAuctionStatus(result.status);
|
|
||||||
setAuctionDetails(result);
|
|
||||||
}, [project.auctionId, project.deployers, project.fundsReleased]);
|
|
||||||
|
|
||||||
const fetchUpdatedProject = useCallback(async () => {
|
|
||||||
const updatedProject = await client.getProject(project.id);
|
|
||||||
setDeployers(updatedProject.project!.deployers!);
|
|
||||||
setFundsStatus(updatedProject.project!.fundsReleased!);
|
|
||||||
}, [project.id]);
|
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
await Promise.all([checkAuctionStatus(), fetchUpdatedProject()]);
|
|
||||||
}, [checkAuctionStatus, fetchUpdatedProject]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
|
|
||||||
const timerId = setInterval(() => {
|
|
||||||
fetchData();
|
|
||||||
}, WAIT_DURATION);
|
|
||||||
|
|
||||||
return () => clearInterval(timerId);
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
const renderAuctionStatus = useCallback(
|
|
||||||
() => (
|
|
||||||
<Tag
|
|
||||||
leftIcon={getIconByAuctionStatus(auctionStatus)}
|
|
||||||
size="xs"
|
|
||||||
type={auctionStatus === 'completed' ? 'positive' : 'emphasized'}
|
|
||||||
>
|
|
||||||
{auctionStatus.toUpperCase()}
|
|
||||||
</Tag>
|
|
||||||
),
|
|
||||||
[auctionStatus],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOpenDialog = () => setOpenDialog(true);
|
|
||||||
const handleCloseDialog = () => setOpenDialog(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="p-3 gap-2 rounded-xl border border-gray-200 transition-colors hover:bg-base-bg-alternate flex flex-col mt-8">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<Heading className="text-lg leading-6 font-medium">
|
|
||||||
Auction details
|
|
||||||
</Heading>
|
|
||||||
<Button onClick={handleOpenDialog} variant="tertiary" size="sm">
|
|
||||||
View details
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center mt-2">
|
|
||||||
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
|
||||||
Auction Id
|
|
||||||
</span>
|
|
||||||
<span className="text-elements-mid-em text-sm text-right">
|
|
||||||
{project.auctionId}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center mt-1">
|
|
||||||
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
|
||||||
Auction Status
|
|
||||||
</span>
|
|
||||||
<div className="ml-2">{renderAuctionStatus()}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{auctionStatus === 'completed' && (
|
|
||||||
<>
|
|
||||||
{deployers?.length > 0 ? (
|
|
||||||
<div>
|
|
||||||
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
|
||||||
Deployer LRNs
|
|
||||||
</span>
|
|
||||||
{deployers.map((deployer, index) => (
|
|
||||||
<p key={index} className="text-elements-mid-em text-sm">
|
|
||||||
{'\u2022'} {deployer.deployerLrn}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center mt-1">
|
|
||||||
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
|
||||||
Deployer Funds Status
|
|
||||||
</span>
|
|
||||||
<div className="ml-2">
|
|
||||||
<Tag
|
|
||||||
size="xs"
|
|
||||||
type={fundsStatus ? 'positive' : 'emphasized'}
|
|
||||||
>
|
|
||||||
{fundsStatus ? 'RELEASED' : 'WAITING'}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="mt-3">
|
|
||||||
<span className="text-elements-high-em text-sm font-medium tracking-tight">
|
|
||||||
No winning deployers
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
open={openDialog}
|
|
||||||
onClose={handleCloseDialog}
|
|
||||||
fullWidth
|
|
||||||
maxWidth="md"
|
|
||||||
>
|
|
||||||
<DialogTitle>Auction Details</DialogTitle>
|
|
||||||
<DialogContent style={DIALOG_STYLE}>
|
|
||||||
{auctionDetails && (
|
|
||||||
<pre>{JSON.stringify(auctionDetails, null, 2)}</pre>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={handleCloseDialog}>Close</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -61,9 +61,7 @@ const DeleteProjectDialog = ({
|
|||||||
<Input
|
<Input
|
||||||
label={
|
label={
|
||||||
"Deleting your project is irreversible. Enter your project's name " +
|
"Deleting your project is irreversible. Enter your project's name " +
|
||||||
'"' +
|
|
||||||
project.name +
|
project.name +
|
||||||
'"' +
|
|
||||||
' below to confirm you want to permanently delete it:'
|
' below to confirm you want to permanently delete it:'
|
||||||
}
|
}
|
||||||
id="input"
|
id="input"
|
||||||
|
@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
import { Domain, DomainStatus, Project } from 'gql-client';
|
import { Domain, DomainStatus, Project } from 'gql-client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Chip,
|
||||||
Typography,
|
Typography,
|
||||||
Menu,
|
Menu,
|
||||||
MenuHandler,
|
MenuHandler,
|
||||||
@ -14,15 +15,6 @@ import EditDomainDialog from './EditDomainDialog';
|
|||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog';
|
import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog';
|
||||||
import { useToast } from 'components/shared/Toast';
|
import { useToast } from 'components/shared/Toast';
|
||||||
import { Tag } from 'components/shared/Tag';
|
|
||||||
import {
|
|
||||||
CheckIcon,
|
|
||||||
CrossIcon,
|
|
||||||
GearIcon,
|
|
||||||
LoadingIcon,
|
|
||||||
} from 'components/shared/CustomIcon';
|
|
||||||
import { Heading } from 'components/shared/Heading';
|
|
||||||
import { Button } from 'components/shared/Button';
|
|
||||||
|
|
||||||
enum RefreshStatus {
|
enum RefreshStatus {
|
||||||
IDLE,
|
IDLE,
|
||||||
@ -87,29 +79,22 @@ const DomainCard = ({
|
|||||||
<>
|
<>
|
||||||
<div className="flex justify-between py-3">
|
<div className="flex justify-between py-3">
|
||||||
<div className="flex justify-start gap-1">
|
<div className="flex justify-start gap-1">
|
||||||
<Heading as="h6" className="flex-col">
|
<Typography variant="h6">
|
||||||
{domain.name}{' '}
|
<i>^</i> {domain.name}
|
||||||
<Tag
|
</Typography>
|
||||||
type={
|
<Chip
|
||||||
domain.status === DomainStatus.Live ? 'positive' : 'negative'
|
className="w-fit capitalize"
|
||||||
}
|
value={domain.status}
|
||||||
leftIcon={
|
color={domain.status === DomainStatus.Live ? 'green' : 'orange'}
|
||||||
domain.status === DomainStatus.Live ? (
|
variant="ghost"
|
||||||
<CheckIcon />
|
icon={<i>^</i>}
|
||||||
) : (
|
/>
|
||||||
<CrossIcon />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{domain.status}
|
|
||||||
</Tag>
|
|
||||||
</Heading>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-start gap-1">
|
<div className="flex justify-start gap-1">
|
||||||
<i
|
<i
|
||||||
id="refresh"
|
id="refresh"
|
||||||
className="cursor-pointer"
|
className="cursor-pointer w-8 h-8"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
SetRefreshStatus(RefreshStatus.CHECKING);
|
SetRefreshStatus(RefreshStatus.CHECKING);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -117,17 +102,11 @@ const DomainCard = ({
|
|||||||
}, CHECK_FAIL_TIMEOUT);
|
}, CHECK_FAIL_TIMEOUT);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{refreshStatus === RefreshStatus.CHECKING ? (
|
{refreshStatus === RefreshStatus.CHECKING ? 'L' : 'R'}
|
||||||
<LoadingIcon className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
'L'
|
|
||||||
)}
|
|
||||||
</i>
|
</i>
|
||||||
<Menu placement="bottom-end">
|
<Menu placement="bottom-end">
|
||||||
<MenuHandler>
|
<MenuHandler>
|
||||||
<Button iconOnly>
|
<button className="border-2 rounded-full w-8 h-8">...</button>
|
||||||
<GearIcon />
|
|
||||||
</Button>
|
|
||||||
</MenuHandler>
|
</MenuHandler>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -164,13 +143,13 @@ const DomainCard = ({
|
|||||||
{domain.status === DomainStatus.Pending && (
|
{domain.status === DomainStatus.Pending && (
|
||||||
<Card className="bg-slate-100 p-4 text-sm">
|
<Card className="bg-slate-100 p-4 text-sm">
|
||||||
{refreshStatus === RefreshStatus.IDLE ? (
|
{refreshStatus === RefreshStatus.IDLE ? (
|
||||||
<Heading>
|
<Typography variant="small">
|
||||||
^ Add these records to your domain and refresh to check
|
^ Add these records to your domain and refresh to check
|
||||||
</Heading>
|
</Typography>
|
||||||
) : refreshStatus === RefreshStatus.CHECKING ? (
|
) : refreshStatus === RefreshStatus.CHECKING ? (
|
||||||
<Heading className="text-blue-500">
|
<Typography variant="small" className="text-blue-500">
|
||||||
^ Checking records for {domain.name}
|
^ Checking records for {domain.name}
|
||||||
</Heading>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex gap-2 text-red-500 mb-2">
|
<div className="flex gap-2 text-red-500 mb-2">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Controller, useForm, SubmitHandler } from 'react-hook-form';
|
import { Controller, useForm, SubmitHandler } from 'react-hook-form';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import { Domain } from 'gql-client';
|
import { Domain } from 'gql-client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -8,11 +9,10 @@ import {
|
|||||||
Option,
|
Option,
|
||||||
} from '@snowballtools/material-tailwind-react-fork';
|
} from '@snowballtools/material-tailwind-react-fork';
|
||||||
|
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||||
import { Modal } from 'components/shared/Modal';
|
import { Modal } from 'components/shared/Modal';
|
||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import { Input } from 'components/shared/Input';
|
import { Input } from 'components/shared/Input';
|
||||||
import { useToast } from 'components/shared/Toast';
|
|
||||||
|
|
||||||
const DEFAULT_REDIRECT_OPTIONS = ['none'];
|
const DEFAULT_REDIRECT_OPTIONS = ['none'];
|
||||||
|
|
||||||
@ -40,7 +40,6 @@ const EditDomainDialog = ({
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
}: EditDomainDialogProp) => {
|
}: EditDomainDialogProp) => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
const { toast, dismiss } = useToast();
|
|
||||||
|
|
||||||
const getRedirectUrl = (domain: Domain) => {
|
const getRedirectUrl = (domain: Domain) => {
|
||||||
const redirectDomain = domain.redirectTo;
|
const redirectDomain = domain.redirectTo;
|
||||||
@ -100,20 +99,10 @@ const EditDomainDialog = ({
|
|||||||
|
|
||||||
if (updateDomain) {
|
if (updateDomain) {
|
||||||
await onUpdate();
|
await onUpdate();
|
||||||
toast({
|
toast.success(`Domain ${domain.name} has been updated`);
|
||||||
id: 'domain_id_updated',
|
|
||||||
title: `Domain ${domain.name} has been updated`,
|
|
||||||
variant: 'success',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
reset();
|
reset();
|
||||||
toast({
|
toast.error(`Error updating domain ${domain.name}`);
|
||||||
id: 'domain_id_error_update',
|
|
||||||
title: `Error updating domain ${domain.name}`,
|
|
||||||
variant: 'error',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOpen();
|
handleOpen();
|
||||||
|
@ -3,8 +3,6 @@ import { useState } from 'react';
|
|||||||
import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog';
|
import { DeleteWebhookDialog } from 'components/projects/Dialog/DeleteWebhookDialog';
|
||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import { useToast } from 'components/shared/Toast';
|
import { useToast } from 'components/shared/Toast';
|
||||||
import { Input } from 'components/shared/Input';
|
|
||||||
import { CopyIcon, TrashIcon } from 'components/shared/CustomIcon';
|
|
||||||
|
|
||||||
interface WebhookCardProps {
|
interface WebhookCardProps {
|
||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
@ -16,12 +14,11 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
|||||||
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between w-full mb-3 gap-3">
|
<div className="flex justify-between w-full mb-3">
|
||||||
<Input value={webhookUrl} disabled />
|
{webhookUrl}
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button
|
<Button
|
||||||
iconOnly
|
size="sm"
|
||||||
size="md"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(webhookUrl);
|
navigator.clipboard.writeText(webhookUrl);
|
||||||
toast({
|
toast({
|
||||||
@ -32,17 +29,16 @@ const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon />
|
Copy
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
iconOnly
|
size="sm"
|
||||||
size="md"
|
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDeleteDialogOpen(true);
|
setDeleteDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TrashIcon />
|
X
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<DeleteWebhookDialog
|
<DeleteWebhookDialog
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { NavLink, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { User } from 'gql-client';
|
import { Organization, User } from 'gql-client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useDisconnect } from 'wagmi';
|
|
||||||
|
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
import {
|
import {
|
||||||
@ -19,7 +18,9 @@ import { getInitials } from 'utils/geInitials';
|
|||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import { cn } from 'utils/classnames';
|
import { cn } from 'utils/classnames';
|
||||||
import { useMediaQuery } from 'usehooks-ts';
|
import { useMediaQuery } from 'usehooks-ts';
|
||||||
import { BASE_URL } from 'utils/constants';
|
import { SIDEBAR_MENU } from './constants';
|
||||||
|
import { UserSelect } from 'components/shared/UserSelect';
|
||||||
|
import { baseUrl } from 'utils/constants';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
mobileOpen?: boolean;
|
mobileOpen?: boolean;
|
||||||
@ -32,7 +33,6 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
|||||||
const isDesktop = useMediaQuery('(min-width: 960px)');
|
const isDesktop = useMediaQuery('(min-width: 960px)');
|
||||||
|
|
||||||
const [user, setUser] = useState<User>();
|
const [user, setUser] = useState<User>();
|
||||||
const { disconnect } = useDisconnect();
|
|
||||||
|
|
||||||
const fetchUser = useCallback(async () => {
|
const fetchUser = useCallback(async () => {
|
||||||
const { user } = await client.getUser();
|
const { user } = await client.getUser();
|
||||||
@ -43,15 +43,54 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
|||||||
fetchUser();
|
fetchUser();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [selectedOrgSlug, setSelectedOrgSlug] = useState(orgSlug);
|
||||||
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
|
|
||||||
|
const fetchUserOrganizations = useCallback(async () => {
|
||||||
|
const { organizations } = await client.getOrganizations();
|
||||||
|
setOrganizations(organizations);
|
||||||
|
}, [orgSlug]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUserOrganizations();
|
||||||
|
setSelectedOrgSlug(orgSlug);
|
||||||
|
}, [orgSlug]);
|
||||||
|
|
||||||
|
const formattedSelected = useMemo(() => {
|
||||||
|
const selected = organizations.find((org) => org.slug === selectedOrgSlug);
|
||||||
|
return {
|
||||||
|
value: selected?.slug ?? '',
|
||||||
|
label: selected?.name ?? '',
|
||||||
|
imgSrc: '/logo.svg',
|
||||||
|
};
|
||||||
|
}, [organizations, selectedOrgSlug, orgSlug]);
|
||||||
|
|
||||||
|
const formattedSelectOptions = useMemo(() => {
|
||||||
|
return organizations.map((org) => ({
|
||||||
|
value: org.slug,
|
||||||
|
label: org.name,
|
||||||
|
imgSrc: '/logo.svg',
|
||||||
|
}));
|
||||||
|
}, [organizations, selectedOrgSlug, orgSlug]);
|
||||||
|
|
||||||
|
const renderMenu = useMemo(() => {
|
||||||
|
return SIDEBAR_MENU(orgSlug).map(({ title, icon, url }, index) => (
|
||||||
|
<NavLink to={url} key={index}>
|
||||||
|
<Tabs.Trigger icon={icon} value={title}>
|
||||||
|
{title}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</NavLink>
|
||||||
|
));
|
||||||
|
}, [orgSlug]);
|
||||||
|
|
||||||
const handleLogOut = useCallback(async () => {
|
const handleLogOut = useCallback(async () => {
|
||||||
await fetch(`${BASE_URL}/auth/logout`, {
|
await fetch(`${baseUrl}/auth/logout`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
disconnect();
|
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
}, [disconnect, navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.nav
|
<motion.nav
|
||||||
@ -75,8 +114,16 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
|||||||
<div className="hidden lg:flex">
|
<div className="hidden lg:flex">
|
||||||
<Logo orgSlug={orgSlug} />
|
<Logo orgSlug={orgSlug} />
|
||||||
</div>
|
</div>
|
||||||
{/* This element ensures the space between logo and navigation */}
|
{/* Switch organization */}
|
||||||
<div className="flex-1"></div>
|
<div className="flex flex-1 flex-col gap-4">
|
||||||
|
<UserSelect
|
||||||
|
value={formattedSelected}
|
||||||
|
options={formattedSelectOptions}
|
||||||
|
/>
|
||||||
|
<Tabs defaultValue="Projects" orientation="vertical">
|
||||||
|
<Tabs.List>{renderMenu}</Tabs.List>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
{/* Bottom navigation */}
|
{/* Bottom navigation */}
|
||||||
<div className="flex flex-col gap-5 justify-end">
|
<div className="flex flex-col gap-5 justify-end">
|
||||||
<Tabs defaultValue="Projects" orientation="vertical">
|
<Tabs defaultValue="Projects" orientation="vertical">
|
||||||
|
@ -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,210 +0,0 @@
|
|||||||
import {
|
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import SignClient from '@walletconnect/sign-client';
|
|
||||||
import { getSdkError } from '@walletconnect/utils';
|
|
||||||
import { SessionTypes } from '@walletconnect/types';
|
|
||||||
|
|
||||||
import { walletConnectModal } from '../utils/web3modal';
|
|
||||||
import {
|
|
||||||
VITE_LACONICD_CHAIN_ID,
|
|
||||||
VITE_WALLET_CONNECT_ID,
|
|
||||||
} from 'utils/constants';
|
|
||||||
|
|
||||||
interface ClientInterface {
|
|
||||||
signClient: SignClient | undefined;
|
|
||||||
session: SessionTypes.Struct | undefined;
|
|
||||||
loadingSession: boolean;
|
|
||||||
onConnect: () => Promise<void>;
|
|
||||||
onDisconnect: () => Promise<void>;
|
|
||||||
onSessionDelete: () => void;
|
|
||||||
accounts: { address: string }[] | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ClientContext = createContext({} as ClientInterface);
|
|
||||||
|
|
||||||
export const useWalletConnectClient = () => {
|
|
||||||
return useContext(ClientContext);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WalletConnectClientProvider = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: JSX.Element;
|
|
||||||
}) => {
|
|
||||||
const [signClient, setSignClient] = useState<SignClient>();
|
|
||||||
const [session, setSession] = useState<SessionTypes.Struct>();
|
|
||||||
const [loadingSession, setLoadingSession] = useState(true);
|
|
||||||
const [accounts, setAccounts] = useState<{ address: string }[]>();
|
|
||||||
|
|
||||||
const isSignClientInitializing = useRef<boolean>(false);
|
|
||||||
|
|
||||||
const onSessionConnect = useCallback(async (session: SessionTypes.Struct) => {
|
|
||||||
setSession(session);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const subscribeToEvents = useCallback(
|
|
||||||
async (client: SignClient) => {
|
|
||||||
client.on('session_update', ({ topic, params }) => {
|
|
||||||
const { namespaces } = params;
|
|
||||||
const currentSession = client.session.get(topic);
|
|
||||||
const updatedSession = { ...currentSession, namespaces };
|
|
||||||
setSession(updatedSession);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[setSession],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onConnect = async () => {
|
|
||||||
const proposalNamespace = {
|
|
||||||
cosmos: {
|
|
||||||
methods: ['cosmos_sendTokens'],
|
|
||||||
chains: [`cosmos:${VITE_LACONICD_CHAIN_ID}`],
|
|
||||||
events: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { uri, approval } = await signClient!.connect({
|
|
||||||
requiredNamespaces: proposalNamespace,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (uri) {
|
|
||||||
walletConnectModal.openModal({ uri });
|
|
||||||
const session = await approval();
|
|
||||||
onSessionConnect(session);
|
|
||||||
walletConnectModal.closeModal();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDisconnect = useCallback(async () => {
|
|
||||||
if (typeof signClient === 'undefined') {
|
|
||||||
throw new Error('WalletConnect is not initialized');
|
|
||||||
}
|
|
||||||
if (typeof session === 'undefined') {
|
|
||||||
throw new Error('Session is not connected');
|
|
||||||
}
|
|
||||||
|
|
||||||
await signClient.disconnect({
|
|
||||||
topic: session.topic,
|
|
||||||
reason: getSdkError('USER_DISCONNECTED'),
|
|
||||||
});
|
|
||||||
|
|
||||||
onSessionDelete();
|
|
||||||
}, [signClient, session]);
|
|
||||||
|
|
||||||
const onSessionDelete = () => {
|
|
||||||
setAccounts(undefined);
|
|
||||||
setSession(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkPersistedState = useCallback(
|
|
||||||
async (signClient: SignClient) => {
|
|
||||||
if (typeof signClient === 'undefined') {
|
|
||||||
throw new Error('WalletConnect is not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof session !== 'undefined') return;
|
|
||||||
if (signClient.session.length) {
|
|
||||||
const lastKeyIndex = signClient.session.keys.length - 1;
|
|
||||||
const previousSsession = signClient.session.get(
|
|
||||||
signClient.session.keys[lastKeyIndex],
|
|
||||||
);
|
|
||||||
|
|
||||||
await onSessionConnect(previousSsession);
|
|
||||||
return previousSsession;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[session, onSessionConnect],
|
|
||||||
);
|
|
||||||
|
|
||||||
const createClient = useCallback(async () => {
|
|
||||||
isSignClientInitializing.current = true;
|
|
||||||
try {
|
|
||||||
const signClient = await SignClient.init({
|
|
||||||
projectId: VITE_WALLET_CONNECT_ID,
|
|
||||||
metadata: {
|
|
||||||
name: 'Deploy App',
|
|
||||||
description: '',
|
|
||||||
url: window.location.href,
|
|
||||||
icons: ['https://avatars.githubusercontent.com/u/92608123'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setSignClient(signClient);
|
|
||||||
await checkPersistedState(signClient);
|
|
||||||
await subscribeToEvents(signClient);
|
|
||||||
setLoadingSession(false);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('error in createClient', e);
|
|
||||||
}
|
|
||||||
isSignClientInitializing.current = false;
|
|
||||||
}, [setSignClient, checkPersistedState, subscribeToEvents]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!signClient && !isSignClientInitializing.current) {
|
|
||||||
createClient();
|
|
||||||
}
|
|
||||||
}, [signClient, createClient]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const populateAccounts = async () => {
|
|
||||||
if (!session) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!session.namespaces['cosmos']) {
|
|
||||||
console.log('Accounts for cosmos namespace not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cosmosAddresses = session.namespaces['cosmos'].accounts;
|
|
||||||
|
|
||||||
const cosmosAccounts = cosmosAddresses.map((address) => ({
|
|
||||||
address,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const allAccounts = cosmosAccounts;
|
|
||||||
|
|
||||||
setAccounts(allAccounts);
|
|
||||||
};
|
|
||||||
|
|
||||||
populateAccounts();
|
|
||||||
}, [session]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!signClient) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
signClient.on('session_delete', onSessionDelete);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
signClient.off('session_delete', onSessionDelete);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClientContext.Provider
|
|
||||||
value={{
|
|
||||||
signClient,
|
|
||||||
onConnect,
|
|
||||||
onDisconnect,
|
|
||||||
onSessionDelete,
|
|
||||||
loadingSession,
|
|
||||||
session,
|
|
||||||
accounts,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ClientContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,116 +1,16 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import assert from 'assert';
|
|
||||||
import { SiweMessage, generateNonce } from 'siwe';
|
|
||||||
import { WagmiProvider } from 'wagmi';
|
|
||||||
import { mainnet } from 'wagmi/chains';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
import { createWeb3Modal } from '@web3modal/wagmi/react';
|
|
||||||
import { defaultWagmiConfig } from '@web3modal/wagmi/react/config';
|
|
||||||
import { createSIWEConfig } from '@web3modal/siwe';
|
|
||||||
import type {
|
|
||||||
SIWECreateMessageArgs,
|
|
||||||
SIWEVerifyMessageArgs,
|
|
||||||
} from '@web3modal/core';
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { VITE_WALLET_CONNECT_ID, BASE_URL } from 'utils/constants';
|
import { VITE_WALLET_CONNECT_ID } from 'utils/constants';
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
if (!VITE_WALLET_CONNECT_ID) {
|
if (!VITE_WALLET_CONNECT_ID) {
|
||||||
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
|
throw new Error('Error: REACT_APP_WALLET_CONNECT_ID env config is not set');
|
||||||
}
|
}
|
||||||
assert(BASE_URL, 'VITE_SERVER_URL is not set in env');
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
export default function Web3Provider({ children }: { children: ReactNode }) {
|
||||||
const axiosInstance = axios.create({
|
|
||||||
baseURL: BASE_URL,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
},
|
|
||||||
withCredentials: true,
|
|
||||||
});
|
|
||||||
const metadata = {
|
|
||||||
name: 'Deploy App Auth',
|
|
||||||
description: '',
|
|
||||||
url: window.location.origin,
|
|
||||||
icons: ['https://avatars.githubusercontent.com/u/37784886'],
|
|
||||||
};
|
|
||||||
const chains = [mainnet] as const;
|
|
||||||
const config = defaultWagmiConfig({
|
|
||||||
chains,
|
|
||||||
projectId: VITE_WALLET_CONNECT_ID,
|
|
||||||
metadata,
|
|
||||||
});
|
|
||||||
const siweConfig = createSIWEConfig({
|
|
||||||
createMessage: ({ nonce, address, chainId }: SIWECreateMessageArgs) =>
|
|
||||||
new SiweMessage({
|
|
||||||
version: '1',
|
|
||||||
domain: window.location.host,
|
|
||||||
uri: window.location.origin,
|
|
||||||
address,
|
|
||||||
chainId,
|
|
||||||
nonce,
|
|
||||||
// Human-readable ASCII assertion that the user will sign, and it must not contain `\n`.
|
|
||||||
statement: 'Sign in With Ethereum.',
|
|
||||||
}).prepareMessage(),
|
|
||||||
getNonce: async () => {
|
|
||||||
return generateNonce();
|
|
||||||
},
|
|
||||||
getSession: async () => {
|
|
||||||
try {
|
|
||||||
const session = (await axiosInstance.get('/auth/session')).data;
|
|
||||||
const { address, chainId } = session;
|
|
||||||
return { address, chainId };
|
|
||||||
} catch (err) {
|
|
||||||
if (window.location.pathname !== '/login') {
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
throw new Error('Failed to get session!');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
verifyMessage: async ({ message, signature }: SIWEVerifyMessageArgs) => {
|
|
||||||
try {
|
|
||||||
const { success } = (
|
|
||||||
await axiosInstance.post('/auth/validate', {
|
|
||||||
message,
|
|
||||||
signature,
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
return success;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
signOut: async () => {
|
|
||||||
try {
|
|
||||||
const { success } = (await axiosInstance.post('/auth/logout')).data;
|
|
||||||
return success;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSignOut: () => {
|
|
||||||
window.location.href = '/login';
|
|
||||||
},
|
|
||||||
onSignIn: () => {
|
|
||||||
window.location.href = '/';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
createWeb3Modal({
|
|
||||||
siweConfig,
|
|
||||||
wagmiConfig: config,
|
|
||||||
projectId: VITE_WALLET_CONNECT_ID,
|
|
||||||
});
|
|
||||||
export default function Web3ModalProvider({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<WagmiProvider config={config}>
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
||||||
</WagmiProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,34 +14,29 @@ import { GQLClientProvider } from './context/GQLClientContext';
|
|||||||
import { SERVER_GQL_PATH } from './constants';
|
import { SERVER_GQL_PATH } from './constants';
|
||||||
import { Toaster } from 'components/shared/Toast';
|
import { Toaster } from 'components/shared/Toast';
|
||||||
import { LogErrorBoundary } from 'utils/log-error';
|
import { LogErrorBoundary } from 'utils/log-error';
|
||||||
import { BASE_URL } from 'utils/constants';
|
import { baseUrl } from 'utils/constants';
|
||||||
import Web3ModalProvider from './context/Web3Provider';
|
|
||||||
import { WalletConnectClientProvider } from 'context/WalletConnectContext';
|
|
||||||
|
|
||||||
console.log(`v-0.0.9`);
|
// @ts-ignore
|
||||||
|
console.log(`v-${__VERSION__}`);
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement,
|
document.getElementById('root') as HTMLElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(BASE_URL, 'VITE_SERVER_URL is not set in env');
|
assert(baseUrl, 'VITE_SERVER_URL is not set in env');
|
||||||
const gqlEndpoint = `${BASE_URL}/${SERVER_GQL_PATH}`;
|
const gqlEndpoint = `${baseUrl}/${SERVER_GQL_PATH}`;
|
||||||
|
|
||||||
const gqlClient = new GQLClient({ gqlEndpoint });
|
const gqlClient = new GQLClient({ gqlEndpoint });
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<LogErrorBoundary>
|
<LogErrorBoundary>
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<WalletConnectClientProvider>
|
<ThemeProvider>
|
||||||
<ThemeProvider>
|
<GQLClientProvider client={gqlClient}>
|
||||||
<Web3ModalProvider>
|
<App />
|
||||||
<GQLClientProvider client={gqlClient}>
|
<Toaster />
|
||||||
<App />
|
</GQLClientProvider>
|
||||||
<Toaster />
|
</ThemeProvider>
|
||||||
</GQLClientProvider>
|
|
||||||
</Web3ModalProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
</WalletConnectClientProvider>
|
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
</LogErrorBoundary>,
|
</LogErrorBoundary>,
|
||||||
);
|
);
|
||||||
|
@ -32,7 +32,7 @@ const ProjectSearch = () => {
|
|||||||
return (
|
return (
|
||||||
<section className="h-full flex flex-col">
|
<section className="h-full flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="sticky hidden lg:block top-0 border-b bg-base-bg border-border-separator/[0.06] 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 pr-6 pl-2 py-2 items-center">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<ProjectSearchBar
|
<ProjectSearchBar
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CloudyFlow } from 'components/CloudyFlow';
|
import { CloudyFlow } from 'components/CloudyFlow';
|
||||||
import { Login } from './auth/Login';
|
import { SnowballAuth } from './auth/SnowballAuth';
|
||||||
|
|
||||||
const AuthPage = () => {
|
const AuthPage = () => {
|
||||||
return (
|
return (
|
||||||
@ -18,7 +18,7 @@ const AuthPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="pb-12 relative z-10 flex-1 flex-center">
|
<div className="pb-12 relative z-10 flex-1 flex-center">
|
||||||
<div className="max-w-[520px] w-full bg-white rounded-xl shadow">
|
<div className="max-w-[520px] w-full bg-white rounded-xl shadow">
|
||||||
<Login />
|
<SnowballAuth />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CloudyFlow>
|
</CloudyFlow>
|
||||||
|
99
packages/frontend/src/pages/auth/AccessCode.tsx
Normal file
99
packages/frontend/src/pages/auth/AccessCode.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
20
packages/frontend/src/pages/auth/AccessSignUp.tsx
Normal file
20
packages/frontend/src/pages/auth/AccessSignUp.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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)} />
|
||||||
|
);
|
||||||
|
};
|
83
packages/frontend/src/pages/auth/CreatePasskey.tsx
Normal file
83
packages/frontend/src/pages/auth/CreatePasskey.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import { LoaderIcon } from 'components/shared/CustomIcon';
|
||||||
|
import { KeyIcon } from 'components/shared/CustomIcon/KeyIcon';
|
||||||
|
import { InlineNotification } from 'components/shared/InlineNotification';
|
||||||
|
import { Input } from 'components/shared/Input';
|
||||||
|
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { IconRight } from 'react-day-picker';
|
||||||
|
import { useSnowball } from 'utils/use-snowball';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onDone: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreatePasskey = ({}: Props) => {
|
||||||
|
const snowball = useSnowball();
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
|
||||||
|
const auth = snowball.auth.passkey;
|
||||||
|
const loading = !!auth.state.loading;
|
||||||
|
|
||||||
|
async function createPasskey() {
|
||||||
|
await auth.register(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
|
||||||
|
<div className="w-16 h-16 p-2 bg-sky-100 rounded-[800px] justify-center items-center gap-2 inline-flex">
|
||||||
|
<KeyIcon />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-loose">
|
||||||
|
Create a passkey
|
||||||
|
</div>
|
||||||
|
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
||||||
|
Passkeys allow you to sign in securely without using passwords.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<WavyBorder className="self-stretch" variant="stroke" />
|
||||||
|
<div className="p-6 flex-col justify-center items-center gap-8 inline-flex">
|
||||||
|
<div className="self-stretch h-36 flex-col justify-center items-center gap-2 flex">
|
||||||
|
<div className="self-stretch h-[72px] flex-col justify-start items-start gap-2 flex">
|
||||||
|
<div className="self-stretch h-5 px-1 flex-col justify-start items-start gap-1 flex">
|
||||||
|
<div className="self-stretch text-sky-950 text-sm font-normal font-['Inter'] leading-tight">
|
||||||
|
Give it a name
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={name}
|
||||||
|
onInput={(e: any) => {
|
||||||
|
setName(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{auth.state.error ? (
|
||||||
|
<InlineNotification
|
||||||
|
title={auth.state.error.message}
|
||||||
|
variant="danger"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<InlineNotification
|
||||||
|
title={`Once you press the "Create passkeys" button, you'll receive a prompt to create the passkey.`}
|
||||||
|
variant="info"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
rightIcon={
|
||||||
|
loading ? <LoaderIcon className="animate-spin" /> : <IconRight />
|
||||||
|
}
|
||||||
|
className="self-stretch"
|
||||||
|
disabled={!name || loading}
|
||||||
|
onClick={createPasskey}
|
||||||
|
>
|
||||||
|
Create Passkey
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,100 @@
|
|||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import {
|
||||||
|
ArrowRightCircleFilledIcon,
|
||||||
|
GithubIcon,
|
||||||
|
LinkIcon,
|
||||||
|
LoaderIcon,
|
||||||
|
QuestionMarkRoundFilledIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { GoogleIcon } from 'components/shared/CustomIcon/GoogleIcon';
|
||||||
|
import { DotBorder } from 'components/shared/DotBorder';
|
||||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { CreatePasskey } from './CreatePasskey';
|
||||||
|
import { AppleIcon } from 'components/shared/CustomIcon/AppleIcon';
|
||||||
|
import { KeyIcon } from 'components/shared/CustomIcon/KeyIcon';
|
||||||
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
|
||||||
|
import { signInWithEthereum } from 'utils/siwe';
|
||||||
|
import { useSnowball } from 'utils/use-snowball';
|
||||||
|
import { logError } from 'utils/log-error';
|
||||||
|
|
||||||
|
type Provider = 'google' | 'github' | 'apple' | 'email' | 'passkey';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onDone: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Login = ({ onDone }: Props) => {
|
||||||
|
const snowball = useSnowball();
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
const [provider, setProvider] = useState<Provider | false>(false);
|
||||||
|
|
||||||
|
// const loading = snowball.auth.state.loading && provider;
|
||||||
|
const loading = provider;
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
if (provider === 'email') {
|
||||||
|
return <CreatePasskey onDone={onDone} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSigninRedirect() {
|
||||||
|
let wallet: PKPEthersWallet | undefined;
|
||||||
|
const { google } = snowball.auth;
|
||||||
|
if (google.canHandleOAuthRedirectBack()) {
|
||||||
|
setProvider('google');
|
||||||
|
console.log('Handling google redirect back');
|
||||||
|
try {
|
||||||
|
await google.handleOAuthRedirectBack();
|
||||||
|
// @ts-ignore
|
||||||
|
wallet = await google.getEthersWallet();
|
||||||
|
// @ts-ignore
|
||||||
|
const result = await signInWithEthereum(1, 'login', wallet);
|
||||||
|
if (result.error) {
|
||||||
|
setError(result.error);
|
||||||
|
setProvider(false);
|
||||||
|
wallet = undefined;
|
||||||
|
logError(new Error(result.error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message);
|
||||||
|
logError(err);
|
||||||
|
setProvider(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if (apple.canHandleOAuthRedirectBack()) {
|
||||||
|
// setProvider('apple');
|
||||||
|
// console.log('Handling apple redirect back');
|
||||||
|
// try {
|
||||||
|
// await apple.handleOAuthRedirectBack();
|
||||||
|
// wallet = await apple.getEthersWallet();
|
||||||
|
// const result = await signInWithEthereum(1, 'login', wallet);
|
||||||
|
// if (result.error) {
|
||||||
|
// setError(result.error);
|
||||||
|
// setProvider(false);
|
||||||
|
// wallet = undefined;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// } catch (err: any) {
|
||||||
|
// setError(err.message);
|
||||||
|
// console.log(err.message, err.name, err.details);
|
||||||
|
// setProvider(false);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (wallet) {
|
||||||
|
window.location.pathname = '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleSigninRedirect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
export const Login = () => {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
|
<div className="self-stretch p-3 xs:p-6 flex-col justify-center items-center gap-5 flex">
|
||||||
@ -11,8 +105,160 @@ export const Login = () => {
|
|||||||
<WavyBorder className="self-stretch" variant="stroke" />
|
<WavyBorder className="self-stretch" variant="stroke" />
|
||||||
|
|
||||||
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
|
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
|
||||||
|
<div className="self-stretch p-5 bg-slate-50 rounded-xl shadow flex-col justify-center items-center gap-6 flex">
|
||||||
|
<div className="self-stretch flex-col justify-center items-center gap-4 flex">
|
||||||
|
<KeyIcon />
|
||||||
|
<div className="self-stretch flex-col justify-center items-center gap-2 flex">
|
||||||
|
<div className="self-stretch text-center text-sky-950 text-lg font-medium font-display leading-normal">
|
||||||
|
Got a Passkey?
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
||||||
|
Use it to sign in securely without using a password.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="self-stretch justify-center items-stretch xxs:items-center gap-3 flex flex-col xxs:flex-row">
|
||||||
|
<Button
|
||||||
|
as="a"
|
||||||
|
leftIcon={<QuestionMarkRoundFilledIcon />}
|
||||||
|
variant={'tertiary'}
|
||||||
|
target="_blank"
|
||||||
|
href="https://safety.google/authentication/passkey/"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
rightIcon={
|
||||||
|
loading && loading === 'passkey' ? (
|
||||||
|
<LoaderIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowRightCircleFilledIcon height="16" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="flex-1"
|
||||||
|
disabled={!!loading}
|
||||||
|
onClick={async () => {
|
||||||
|
setProvider('passkey');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sign In with Passkey
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-5 justify-center items-center gap-2 inline-flex">
|
||||||
|
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
||||||
|
Lost your passkey?
|
||||||
|
</div>
|
||||||
|
<div className="justify-center items-center gap-1.5 flex">
|
||||||
|
<button className="text-sky-950 text-sm font-normal font-['Inter'] underline leading-tight">
|
||||||
|
Recover account
|
||||||
|
</button>
|
||||||
|
<LinkIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="self-stretch justify-start items-center gap-8 inline-flex">
|
||||||
|
<DotBorder className="flex-1" />
|
||||||
|
<div className="text-center text-slate-400 text-xs font-normal font-['JetBrains Mono'] leading-none">
|
||||||
|
OR
|
||||||
|
</div>
|
||||||
|
<DotBorder className="flex-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="self-stretch flex-col justify-center items-center gap-3 flex">
|
<div className="self-stretch flex-col justify-center items-center gap-3 flex">
|
||||||
<w3m-button />
|
<Button
|
||||||
|
leftIcon={<GoogleIcon />}
|
||||||
|
rightIcon={
|
||||||
|
loading && loading === 'google' ? (
|
||||||
|
<LoaderIcon className="animate-spin" />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setProvider('google');
|
||||||
|
snowball.auth.google.startOAuthRedirect();
|
||||||
|
}}
|
||||||
|
className="flex-1 self-stretch"
|
||||||
|
variant={'tertiary'}
|
||||||
|
disabled={!!loading}
|
||||||
|
>
|
||||||
|
Continue with Google
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
leftIcon={<GithubIcon />}
|
||||||
|
rightIcon={
|
||||||
|
loading && loading === 'github' ? (
|
||||||
|
<LoaderIcon className="animate-spin" />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
onClick={async () => {
|
||||||
|
setProvider('github');
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
|
setProvider(false);
|
||||||
|
toast({
|
||||||
|
id: 'coming-soon',
|
||||||
|
title: 'Sign-in with GitHub is coming soon!',
|
||||||
|
variant: 'info',
|
||||||
|
onDismiss() {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="flex-1 self-stretch"
|
||||||
|
variant={'tertiary'}
|
||||||
|
disabled={!!loading}
|
||||||
|
>
|
||||||
|
Continue with GitHub
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
leftIcon={<AppleIcon />}
|
||||||
|
rightIcon={
|
||||||
|
loading && loading === 'apple' ? (
|
||||||
|
<LoaderIcon className="animate-spin text-white" />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
onClick={async () => {
|
||||||
|
setProvider('apple');
|
||||||
|
// snowball.auth.apple.startOAuthRedirect();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
|
setProvider(false);
|
||||||
|
toast({
|
||||||
|
id: 'coming-soon',
|
||||||
|
title: 'Sign-in with Apple is coming soon!',
|
||||||
|
variant: 'info',
|
||||||
|
onDismiss() {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className={`flex-1 self-stretch border-black enabled:bg-black text-white ${
|
||||||
|
loading && loading === 'apple' ? 'disabled:bg-black' : ''
|
||||||
|
}`}
|
||||||
|
variant={'tertiary'}
|
||||||
|
disabled={!!loading}
|
||||||
|
>
|
||||||
|
Continue with Apple
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{error && (
|
||||||
|
<div className="justify-center items-center gap-2 inline-flex">
|
||||||
|
<div className="text-red-500 text-sm">Error: {error}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="h-5 justify-center items-center gap-2 inline-flex">
|
||||||
|
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
||||||
|
Don't have an account?
|
||||||
|
</div>
|
||||||
|
<div className="justify-center items-center gap-1.5 flex">
|
||||||
|
<Link
|
||||||
|
to="/signup"
|
||||||
|
className="text-sky-950 text-sm font-normal font-['Inter'] underline leading-tight"
|
||||||
|
>
|
||||||
|
Sign up now
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
287
packages/frontend/src/pages/auth/SignUp.tsx
Normal file
287
packages/frontend/src/pages/auth/SignUp.tsx
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import {
|
||||||
|
ArrowRightCircleFilledIcon,
|
||||||
|
GithubIcon,
|
||||||
|
LoaderIcon,
|
||||||
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { GoogleIcon } from 'components/shared/CustomIcon/GoogleIcon';
|
||||||
|
import { DotBorder } from 'components/shared/DotBorder';
|
||||||
|
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useSnowball } from 'utils/use-snowball';
|
||||||
|
import { Input } from 'components/shared/Input';
|
||||||
|
import { AppleIcon } from 'components/shared/CustomIcon/AppleIcon';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
import { PKPEthersWallet } from '@lit-protocol/pkp-ethers';
|
||||||
|
import { signInWithEthereum } from 'utils/siwe';
|
||||||
|
import { logError } from 'utils/log-error';
|
||||||
|
import {
|
||||||
|
subOrganizationIdForEmail,
|
||||||
|
turnkeySignin,
|
||||||
|
turnkeySignup,
|
||||||
|
} from 'utils/turnkey-frontend';
|
||||||
|
import { verifyAccessCode } from 'utils/accessCode';
|
||||||
|
|
||||||
|
type Provider = 'google' | 'github' | 'apple' | 'email';
|
||||||
|
|
||||||
|
type Err = { type: 'email' | 'provider'; message: string };
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onDone: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SignUp = ({ onDone }: Props) => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [error, setError] = useState<Err | null>();
|
||||||
|
const [provider, setProvider] = useState<Provider | false>(false);
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
const snowball = useSnowball();
|
||||||
|
|
||||||
|
async function handleSignupRedirect() {
|
||||||
|
let wallet: PKPEthersWallet | undefined;
|
||||||
|
const { google } = snowball.auth;
|
||||||
|
if (google.canHandleOAuthRedirectBack()) {
|
||||||
|
setProvider('google');
|
||||||
|
try {
|
||||||
|
await google.handleOAuthRedirectBack();
|
||||||
|
// @ts-ignore
|
||||||
|
wallet = await google.getEthersWallet();
|
||||||
|
// @ts-ignore
|
||||||
|
const result = await signInWithEthereum(1, 'signup', wallet);
|
||||||
|
if (result.error) {
|
||||||
|
setError({ type: 'provider', message: result.error });
|
||||||
|
setProvider(false);
|
||||||
|
wallet = undefined;
|
||||||
|
logError(new Error(result.error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setError({ type: 'provider', message: err.message });
|
||||||
|
setProvider(false);
|
||||||
|
logError(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if (apple.canHandleOAuthRedirectBack()) {
|
||||||
|
// setProvider('apple');
|
||||||
|
// try {
|
||||||
|
// await apple.handleOAuthRedirectBack();
|
||||||
|
// wallet = await apple.getEthersWallet();
|
||||||
|
// const result = await signInWithEthereum(1, 'signup', wallet);
|
||||||
|
// if (result.error) {
|
||||||
|
// setError({ type: 'provider', message: result.error });
|
||||||
|
// setProvider(false);
|
||||||
|
// wallet = undefined;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// } catch (err: any) {
|
||||||
|
// setError({ type: 'provider', message: err.message });
|
||||||
|
// setProvider(false);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (wallet) {
|
||||||
|
onDone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authEmail() {
|
||||||
|
setProvider('email');
|
||||||
|
try {
|
||||||
|
const orgId = await subOrganizationIdForEmail(email);
|
||||||
|
console.log('orgId', orgId);
|
||||||
|
if (orgId) {
|
||||||
|
await turnkeySignin(orgId);
|
||||||
|
window.location.href = '/dashboard';
|
||||||
|
} else {
|
||||||
|
await turnkeySignup(email);
|
||||||
|
onDone();
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setError({ type: 'email', message: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleSignupRedirect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loading = provider;
|
||||||
|
const emailValid = /.@./.test(email);
|
||||||
|
|
||||||
|
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">
|
||||||
|
<div className="self-stretch text-center text-sky-950 text-2xl font-medium font-display leading-tight">
|
||||||
|
Sign up to Snowball
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<WavyBorder className="self-stretch" variant="stroke" />
|
||||||
|
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
|
||||||
|
<div className="self-stretch flex-col justify-center items-center gap-3 flex">
|
||||||
|
<Button
|
||||||
|
leftIcon={loading && loading === 'google' ? null : <GoogleIcon />}
|
||||||
|
rightIcon={
|
||||||
|
loading && loading === 'google' ? (
|
||||||
|
<LoaderIcon className="animate-spin" />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setProvider('google');
|
||||||
|
snowball.auth.google.startOAuthRedirect();
|
||||||
|
}}
|
||||||
|
className="flex-1 self-stretch"
|
||||||
|
variant={'tertiary'}
|
||||||
|
disabled={!!loading}
|
||||||
|
>
|
||||||
|
Continue with Google
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
leftIcon={<GithubIcon />}
|
||||||
|
rightIcon={
|
||||||
|
loading && loading === 'github' ? (
|
||||||
|
<LoaderIcon className="animate-spin" />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
onClick={async () => {
|
||||||
|
setProvider('github');
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
|
setProvider(false);
|
||||||
|
toast({
|
||||||
|
id: 'coming-soon',
|
||||||
|
title: 'Sign-in with GitHub is coming soon!',
|
||||||
|
variant: 'info',
|
||||||
|
onDismiss() {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="flex-1 self-stretch"
|
||||||
|
variant={'tertiary'}
|
||||||
|
disabled={!!loading}
|
||||||
|
>
|
||||||
|
Continue with GitHub
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
leftIcon={<AppleIcon />}
|
||||||
|
rightIcon={
|
||||||
|
loading && loading === 'apple' ? (
|
||||||
|
<LoaderIcon className="animate-spin text-white" />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
onClick={async () => {
|
||||||
|
setProvider('apple');
|
||||||
|
// snowball.auth.apple.startOAuthRedirect();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
|
setProvider(false);
|
||||||
|
toast({
|
||||||
|
id: 'coming-soon',
|
||||||
|
title: 'Sign-in with Apple is coming soon!',
|
||||||
|
variant: 'info',
|
||||||
|
onDismiss() {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className={`flex-1 self-stretch border-black enabled:bg-black text-white ${
|
||||||
|
loading && loading === 'apple' ? 'disabled:bg-black' : ''
|
||||||
|
}`}
|
||||||
|
variant={'tertiary'}
|
||||||
|
disabled={!!loading}
|
||||||
|
>
|
||||||
|
Continue with Apple
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && error.type === 'provider' && (
|
||||||
|
<div className="-mt-3 justify-center items-center inline-flex">
|
||||||
|
<div className="text-red-500 text-sm">Error: {error.message}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="self-stretch justify-start items-center gap-8 inline-flex">
|
||||||
|
<DotBorder className="flex-1" />
|
||||||
|
<div className="text-center text-slate-400 text-xs font-normal font-['JetBrains Mono'] leading-none">
|
||||||
|
OR
|
||||||
|
</div>
|
||||||
|
<DotBorder className="flex-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="self-stretch flex-col gap-8 flex">
|
||||||
|
<div className="flex-col justify-start items-start gap-2 inline-flex">
|
||||||
|
<div className="text-sky-950 text-sm font-normal font-['Inter'] leading-tight">
|
||||||
|
Email
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
disabled={!!loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
rightIcon={
|
||||||
|
loading && loading === 'email' ? (
|
||||||
|
<LoaderIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowRightCircleFilledIcon height="16" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
authEmail();
|
||||||
|
}}
|
||||||
|
variant={'secondary'}
|
||||||
|
disabled={!email || !emailValid || !!loading}
|
||||||
|
>
|
||||||
|
Continue with Email
|
||||||
|
</Button>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{error && error.type === 'email' && (
|
||||||
|
<div className="justify-center items-center gap-2 inline-flex">
|
||||||
|
<div className="text-red-500 text-sm">
|
||||||
|
Error: {error.message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="justify-center items-center gap-2 inline-flex">
|
||||||
|
<div className="text-center text-slate-600 text-sm font-normal font-['Inter'] leading-tight">
|
||||||
|
Already an user?
|
||||||
|
</div>
|
||||||
|
<div className="justify-center items-center gap-1.5 flex">
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
className="text-sky-950 text-sm font-normal font-['Inter'] underline leading-tight"
|
||||||
|
>
|
||||||
|
Sign in now
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
53
packages/frontend/src/pages/auth/SnowballAuth.tsx
Normal file
53
packages/frontend/src/pages/auth/SnowballAuth.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { snowball } from 'utils/use-snowball';
|
||||||
|
import { Login } from './Login';
|
||||||
|
import { Done } from './Done';
|
||||||
|
import { AccessSignUp } from './AccessSignUp';
|
||||||
|
|
||||||
|
type Screen = 'login' | 'signup' | 'success';
|
||||||
|
|
||||||
|
const DASHBOARD_URL = '/';
|
||||||
|
|
||||||
|
export const SnowballAuth: React.FC = () => {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
const [screen, setScreen] = useState<Screen>(
|
||||||
|
path === '/login' ? 'login' : 'signup',
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (snowball.session) {
|
||||||
|
window.location.href = DASHBOARD_URL;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (path === '/login') {
|
||||||
|
setScreen('login');
|
||||||
|
} else if (path === '/signup') {
|
||||||
|
setScreen('signup');
|
||||||
|
}
|
||||||
|
}, [path]);
|
||||||
|
|
||||||
|
if (screen === 'signup') {
|
||||||
|
return (
|
||||||
|
<AccessSignUp
|
||||||
|
onDone={() => {
|
||||||
|
setScreen('success');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (screen === 'login') {
|
||||||
|
return (
|
||||||
|
<Login
|
||||||
|
onDone={() => {
|
||||||
|
setScreen('success');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (screen === 'success') {
|
||||||
|
return <Done continueTo={DASHBOARD_URL} />;
|
||||||
|
}
|
||||||
|
};
|
@ -36,13 +36,6 @@ const deployment: Deployment = {
|
|||||||
url: 'https://deploy1.example.com',
|
url: 'https://deploy1.example.com',
|
||||||
environment: Environment.Production,
|
environment: Environment.Production,
|
||||||
isCurrent: true,
|
isCurrent: true,
|
||||||
deployer: {
|
|
||||||
deployerApiUrl: 'https://webapp-deployer-api.example.com',
|
|
||||||
deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu',
|
|
||||||
deployerLrn: 'lrn://example/deployers/webapp-deployer-api.example.com',
|
|
||||||
minimumPayment: '1000alnt',
|
|
||||||
baseDomain: 'pwa.example.com',
|
|
||||||
},
|
|
||||||
status: DeploymentStatus.Ready,
|
status: DeploymentStatus.Ready,
|
||||||
createdBy: {
|
createdBy: {
|
||||||
id: 'user1',
|
id: 'user1',
|
||||||
@ -55,8 +48,6 @@ const deployment: Deployment = {
|
|||||||
},
|
},
|
||||||
createdAt: '1677676800', // 2023-03-01T12:00:00Z
|
createdAt: '1677676800', // 2023-03-01T12:00:00Z
|
||||||
updatedAt: '1677680400', // 2023-03-01T13:00:00Z
|
updatedAt: '1677680400', // 2023-03-01T13:00:00Z
|
||||||
applicationDeploymentRequestId:
|
|
||||||
'bafyreiaycvq6imoppnpwdve4smj6t6ql5svt5zl3x6rimu4qwyzgjorize',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const domains: Domain[] = [
|
const domains: Domain[] = [
|
||||||
|
@ -1,19 +1,5 @@
|
|||||||
import { Heading } from 'components/shared/Heading';
|
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
return (
|
return <div className="p-5">Settings page</div>;
|
||||||
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="grow">
|
|
||||||
<div className="flex gap-4 items-center">
|
|
||||||
<Heading as="h2" className="text-[24px]">
|
|
||||||
Settings
|
|
||||||
</Heading>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Settings;
|
export default Settings;
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { 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 { 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 Projects = () => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
@ -26,7 +28,6 @@ const Projects = () => {
|
|||||||
return (
|
return (
|
||||||
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
|
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
|
@ -92,13 +92,9 @@ const Id = () => {
|
|||||||
Open repo
|
Open repo
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{(project.deployments.length > 0) &&
|
<Button {...buttonSize} className="h-11 transition-colors">
|
||||||
<Link to={`https://${project.name.toLowerCase()}.${project.deployments[0].deployer.baseDomain}`}>
|
Go to app
|
||||||
<Button {...buttonSize} className="h-11 transition-colors">
|
</Button>
|
||||||
Go to app
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<WavyBorder />
|
<WavyBorder />
|
||||||
|
@ -31,11 +31,6 @@ const CreateWithTemplate = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
step: 2,
|
step: 2,
|
||||||
route: `/${orgSlug}/projects/create/template/configure`,
|
|
||||||
label: 'Configure',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step: 3,
|
|
||||||
route: `/${orgSlug}/projects/create/template/deploy`,
|
route: `/${orgSlug}/projects/create/template/deploy`,
|
||||||
label: 'Deploy',
|
label: 'Deploy',
|
||||||
},
|
},
|
||||||
|
@ -2,8 +2,7 @@ import NewProject from './index';
|
|||||||
import CreateWithTemplate from './Template';
|
import CreateWithTemplate from './Template';
|
||||||
import { templateRoutes } from './template/routes';
|
import { templateRoutes } from './template/routes';
|
||||||
import Id from './success/Id';
|
import Id from './success/Id';
|
||||||
import Configure from 'components/projects/create/Configure';
|
import Import from './Import';
|
||||||
import Deploy from 'components/projects/create/Deploy';
|
|
||||||
|
|
||||||
export const createProjectRoutes = [
|
export const createProjectRoutes = [
|
||||||
{
|
{
|
||||||
@ -20,11 +19,7 @@ export const createProjectRoutes = [
|
|||||||
element: <Id />,
|
element: <Id />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'configure',
|
path: 'import',
|
||||||
element: <Configure />,
|
element: <Import />,
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'deploy',
|
|
||||||
element: <Deploy />,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -1,123 +1,108 @@
|
|||||||
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import Lottie from 'lottie-react';
|
import Lottie from 'lottie-react';
|
||||||
|
|
||||||
import { Badge } from 'components/shared/Badge';
|
import { Badge } from 'components/shared/Badge';
|
||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import {
|
import {
|
||||||
ArrowLeftCircleFilledIcon,
|
ArrowLeftCircleFilledIcon,
|
||||||
|
LinkChainIcon,
|
||||||
QuestionMarkRoundFilledIcon,
|
QuestionMarkRoundFilledIcon,
|
||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import { Heading } from 'components/shared/Heading';
|
import { Heading } from 'components/shared/Heading';
|
||||||
|
|
||||||
import logoAnimation from 'components/../../public/lottie/logo.json';
|
import logoAnimation from 'components/../../public/lottie/logo.json';
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
|
||||||
import { Project } from 'gql-client';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
const Id = () => {
|
const Id = () => {
|
||||||
const { id, orgSlug } = useParams();
|
const { id, orgSlug } = useParams();
|
||||||
const client = useGQLClient();
|
|
||||||
const [project, setProject] = useState<Project | null>(null);
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const isAuction = searchParams.get('isAuction') === 'true';
|
|
||||||
|
|
||||||
const handleSetupDomain = async () => {
|
const handleSetupDomain = () => {
|
||||||
if (id) {
|
//TODO: Implement this
|
||||||
const project = await client.getProject(id);
|
|
||||||
if (project && project.project) {
|
|
||||||
setProject(project.project);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.location.href = '/';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleSetupDomain();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex flex-col gap-8 lg:gap-11 max-w-[522px] mx-auto py-6 lg:py-12">
|
||||||
{project ? (
|
{/* Icon */}
|
||||||
<div className="flex flex-col gap-8 lg:gap-11 max-w-[522px] mx-auto py-6 lg:py-12">
|
<div className="flex justify-center">
|
||||||
{/* Icon */}
|
<Lottie animationData={logoAnimation} loop={false} size={40} />
|
||||||
<div className="flex justify-center">
|
</div>
|
||||||
<Lottie animationData={logoAnimation} loop={false} size={40} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Heading */}
|
{/* Heading */}
|
||||||
<div className="flex flex-col items-center gap-1.5">
|
<div className="flex flex-col items-center gap-1.5">
|
||||||
<Heading as="h3" className="font-medium text-xl">
|
<Heading as="h3" className="font-medium text-xl">
|
||||||
{isAuction
|
Project deployed successfully.
|
||||||
? 'Auction created successfully.'
|
</Heading>
|
||||||
: 'Project deployment created successfully.'}
|
<p className="flex flex-col items-center lg:flex-row font-sans gap-0.5 lg:gap-2 text-sm text-elements-high-em">
|
||||||
</Heading>
|
Your project has been deployed at{' '}
|
||||||
</div>
|
<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 */}
|
{/* Card */}
|
||||||
<div className="bg-base-bg-alternate rounded-xl shadow-inset w-full px-1 py-1">
|
<div className="bg-base-bg-alternate rounded-xl shadow-inset w-full px-1 py-1">
|
||||||
{/* Trigger question */}
|
{/* Trigger question */}
|
||||||
<div className="flex gap-2 justify-center items-center py-3">
|
<div className="flex gap-2 justify-center items-center py-3">
|
||||||
<div className="h-5 w-5">
|
<div className="h-5 w-5">
|
||||||
<QuestionMarkRoundFilledIcon size={18} />
|
<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>
|
</div>
|
||||||
<Heading as="h5" className="font-sans font-medium text-sm">
|
<Button onClick={handleSetupDomain} variant="tertiary" size="sm">
|
||||||
{`Wondering what's next?`}
|
Setup domain
|
||||||
</Heading>
|
</Button>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CTA card */}
|
|
||||||
<div className="bg-surface-card rounded-xl shadow-card-sm px-4 py-4">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Badge variant="secondary">1</Badge>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<Heading as="h6" className="text-sm font-sans">
|
|
||||||
Add a custom domain
|
|
||||||
</Heading>
|
|
||||||
<p className="text-xs text-elements-low-em font-sans">
|
|
||||||
Make it easy for your visitors to remember your URL with a
|
|
||||||
custom domain.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={handleSetupDomain}
|
|
||||||
variant="tertiary"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Setup domain
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CTA Buttons */}
|
|
||||||
<div className="flex flex-col lg:flex-row justify-center gap-3">
|
|
||||||
<div className="w-full lg:w-fit">
|
|
||||||
<Link to="/">
|
|
||||||
<Button
|
|
||||||
leftIcon={<ArrowLeftCircleFilledIcon />}
|
|
||||||
fullWidth
|
|
||||||
variant="tertiary"
|
|
||||||
>
|
|
||||||
Back to projects
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="w-full lg:w-fit">
|
|
||||||
<Link to={`/${orgSlug}/projects/${id}`}>
|
|
||||||
<Button fullWidth variant="primary">
|
|
||||||
View project
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<></>
|
|
||||||
)}
|
{/* CTA Buttons */}
|
||||||
</>
|
<div className="flex flex-col lg:flex-row justify-center gap-3">
|
||||||
|
<div className="w-full lg:w-fit">
|
||||||
|
<Link to="/">
|
||||||
|
<Button
|
||||||
|
leftIcon={<ArrowLeftCircleFilledIcon />}
|
||||||
|
fullWidth
|
||||||
|
variant="tertiary"
|
||||||
|
>
|
||||||
|
Back to projects
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="w-full lg:w-fit">
|
||||||
|
<Link to={`/${orgSlug}/projects/${id}`}>
|
||||||
|
<Button fullWidth variant="primary">
|
||||||
|
View project
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import ConfigureComponent from '../../../../../components/projects/create/Configure';
|
|
||||||
|
|
||||||
const Configure = () => {
|
|
||||||
return <ConfigureComponent />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Configure;
|
|
@ -6,6 +6,7 @@ import { useMediaQuery } from 'usehooks-ts';
|
|||||||
import { RequestError } from 'octokit';
|
import { RequestError } from 'octokit';
|
||||||
|
|
||||||
import { useOctokit } from '../../../../../context/OctokitContext';
|
import { useOctokit } from '../../../../../context/OctokitContext';
|
||||||
|
import { useGQLClient } from '../../../../../context/GQLClientContext';
|
||||||
import { Template } from '../../../../../types/types';
|
import { Template } from '../../../../../types/types';
|
||||||
import { Heading } from 'components/shared/Heading';
|
import { Heading } from 'components/shared/Heading';
|
||||||
import { Input } from 'components/shared/Input';
|
import { Input } from 'components/shared/Input';
|
||||||
@ -14,6 +15,7 @@ import {
|
|||||||
ArrowRightCircleFilledIcon,
|
ArrowRightCircleFilledIcon,
|
||||||
LoadingIcon,
|
LoadingIcon,
|
||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { Checkbox } from 'components/shared/Checkbox';
|
||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import { useToast } from 'components/shared/Toast';
|
import { useToast } from 'components/shared/Toast';
|
||||||
|
|
||||||
@ -29,6 +31,7 @@ type SubmitRepoValues = {
|
|||||||
const CreateRepo = () => {
|
const CreateRepo = () => {
|
||||||
const { octokit, isAuth } = useOctokit();
|
const { octokit, isAuth } = useOctokit();
|
||||||
const { template } = useOutletContext<{ template: Template }>();
|
const { template } = useOutletContext<{ template: Template }>();
|
||||||
|
const client = useGQLClient();
|
||||||
|
|
||||||
const { orgSlug } = useParams();
|
const { orgSlug } = useParams();
|
||||||
const { toast, dismiss } = useToast();
|
const { toast, dismiss } = useToast();
|
||||||
@ -41,17 +44,6 @@ const CreateRepo = () => {
|
|||||||
const [gitAccounts, setGitAccounts] = useState<string[]>([]);
|
const [gitAccounts, setGitAccounts] = useState<string[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const checkRepoExists = async (account: string, repoName: string) => {
|
|
||||||
try {
|
|
||||||
await octokit.rest.repos.get({ owner: account, repo: repoName });
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
// Error handled by octokit error hook interceptor in Octokit context
|
|
||||||
console.error(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitRepoHandler: SubmitHandler<SubmitRepoValues> = useCallback(
|
const submitRepoHandler: SubmitHandler<SubmitRepoValues> = useCallback(
|
||||||
async (data) => {
|
async (data) => {
|
||||||
assert(data.account);
|
assert(data.account);
|
||||||
@ -61,22 +53,21 @@ const CreateRepo = () => {
|
|||||||
assert(template.repoFullName, 'Template URL not provided');
|
assert(template.repoFullName, 'Template URL not provided');
|
||||||
const [owner, repo] = template.repoFullName.split('/');
|
const [owner, repo] = template.repoFullName.split('/');
|
||||||
|
|
||||||
const repoExists = await checkRepoExists(data.account, data.repoName);
|
|
||||||
if (repoExists) {
|
|
||||||
toast({
|
|
||||||
id: 'repo-exist-error',
|
|
||||||
title: 'Repository already exists with this name',
|
|
||||||
variant: 'warning',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const { addProjectFromTemplate } = await client.addProjectFromTemplate(
|
||||||
|
orgSlug!,
|
||||||
|
{
|
||||||
|
templateOwner: owner,
|
||||||
|
templateRepo: repo,
|
||||||
|
owner: data.account,
|
||||||
|
name: data.repoName,
|
||||||
|
isPrivate: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
navigate(
|
navigate(
|
||||||
`configure?templateId=${template.id}&templateOwner=${owner}&templateRepo=${repo}&owner=${data.account}&name=${data.repoName}&isPrivate=false&orgSlug=${orgSlug}`,
|
`deploy?projectId=${addProjectFromTemplate.id}&templateId=${template.id}`,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -104,7 +95,7 @@ const CreateRepo = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[octokit, toast],
|
[octokit],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -185,12 +176,20 @@ const CreateRepo = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="repoName"
|
name="repoName"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: true }}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<Input value={value} onChange={onChange} />
|
<Input value={value} onChange={onChange} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
name="isPrivate"
|
||||||
|
control={control}
|
||||||
|
render={({}) => (
|
||||||
|
<Checkbox label="Make this repo private" disabled={true} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
{...buttonSize}
|
{...buttonSize}
|
||||||
@ -204,7 +203,7 @@ const CreateRepo = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Next
|
Deploy
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import CreateRepo from './index';
|
import CreateRepo from './index';
|
||||||
import Configure from './Configure';
|
|
||||||
import Deploy from './Deploy';
|
import Deploy from './Deploy';
|
||||||
|
|
||||||
export const templateRoutes = [
|
export const templateRoutes = [
|
||||||
@ -7,10 +6,6 @@ export const templateRoutes = [
|
|||||||
index: true,
|
index: true,
|
||||||
element: <CreateRepo />,
|
element: <CreateRepo />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'configure',
|
|
||||||
element: <Configure />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'deploy',
|
path: 'deploy',
|
||||||
element: <Deploy />,
|
element: <Deploy />,
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Domain, DomainStatus } from 'gql-client';
|
||||||
import { Link, useNavigate, useOutletContext } from 'react-router-dom';
|
import { Link, useNavigate, useOutletContext } from 'react-router-dom';
|
||||||
import { RequestError } from 'octokit';
|
import { RequestError } from 'octokit';
|
||||||
|
|
||||||
import { useOctokit } from 'context/OctokitContext';
|
import { useOctokit } from '../../../../context/OctokitContext';
|
||||||
import { GitCommitWithBranch, OutletContextType } from '../../../../types';
|
import { GitCommitWithBranch, OutletContextType } from '../../../../types';
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||||
import { Button, Heading, Avatar, Tag } from 'components/shared';
|
import { Button } from 'components/shared/Button';
|
||||||
|
import { Heading } from 'components/shared/Heading';
|
||||||
|
import { Avatar } from 'components/shared/Avatar';
|
||||||
import { getInitials } from 'utils/geInitials';
|
import { getInitials } from 'utils/geInitials';
|
||||||
import {
|
import {
|
||||||
BranchStrokeIcon,
|
BranchStrokeIcon,
|
||||||
@ -15,13 +18,12 @@ import {
|
|||||||
GithubStrokeIcon,
|
GithubStrokeIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
CalendarDaysIcon,
|
|
||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
|
import { Tag } from 'components/shared/Tag';
|
||||||
import { Activity } from 'components/projects/project/overview/Activity';
|
import { Activity } from 'components/projects/project/overview/Activity';
|
||||||
import { OverviewInfo } from 'components/projects/project/overview/OverviewInfo';
|
import { OverviewInfo } from 'components/projects/project/overview/OverviewInfo';
|
||||||
|
import { CalendarDaysIcon } from 'components/shared/CustomIcon/CalendarDaysIcon';
|
||||||
import { relativeTimeMs } from 'utils/time';
|
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;
|
const COMMITS_PER_PAGE = 4;
|
||||||
|
|
||||||
@ -129,19 +131,9 @@ const OverviewTabPanel = () => {
|
|||||||
<Heading className="text-lg leading-6 font-medium truncate">
|
<Heading className="text-lg leading-6 font-medium truncate">
|
||||||
{project.name}
|
{project.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
{project.deployments &&
|
<p className="text-sm text-elements-low-em tracking-tight truncate">
|
||||||
project.deployments.length > 0 &&
|
{project.subDomain}
|
||||||
project.deployments.map((deployment, index) => (
|
</p>
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
key={index}
|
|
||||||
href={`https://${project.name.toLowerCase()}.${deployment.deployer.baseDomain}`}
|
|
||||||
className="text-sm text-elements-low-em tracking-tight truncate"
|
|
||||||
>
|
|
||||||
{deployment.deployer.baseDomain}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<OverviewInfo label="Domain" icon={<GlobeIcon />}>
|
<OverviewInfo label="Domain" icon={<GlobeIcon />}>
|
||||||
@ -180,18 +172,14 @@ const OverviewTabPanel = () => {
|
|||||||
|
|
||||||
{/* DEPLOYMENT */}
|
{/* DEPLOYMENT */}
|
||||||
<OverviewInfo label="Deployment URL" icon={<CursorBoxIcon />}>
|
<OverviewInfo label="Deployment URL" icon={<CursorBoxIcon />}>
|
||||||
{project.deployments &&
|
|
||||||
project.deployments.length > 0 &&
|
|
||||||
project.deployments.map((deployment) => (
|
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<Link to={`https://${project.name.toLowerCase()}.${deployment.deployer.baseDomain}`}>
|
<Link to="#">
|
||||||
<span className="text-controls-primary group hover:border-controls-primary transition-colors border-b border-b-transparent flex gap-2 items-center text-sm tracking-tight">
|
<span className="text-controls-primary group hover:border-controls-primary transition-colors border-b border-b-transparent flex gap-2 items-center text-sm tracking-tight">
|
||||||
{`https://${project.name.toLowerCase()}.${deployment.deployer.baseDomain}`}
|
{liveDomain?.name}{' '}
|
||||||
<LinkIcon className="group-hover:rotate-45 transition-transform" />
|
<LinkIcon className="group-hover:rotate-45 transition-transform" />
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</OverviewInfo>
|
</OverviewInfo>
|
||||||
|
|
||||||
{/* DEPLOYMENT DATE */}
|
{/* DEPLOYMENT DATE */}
|
||||||
@ -217,7 +205,6 @@ const OverviewTabPanel = () => {
|
|||||||
No current deployment found.
|
No current deployment found.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{project.auctionId && <AuctionCard project={project} />}
|
|
||||||
</div>
|
</div>
|
||||||
<Activity activities={activities} isLoading={fetchingActivities} />
|
<Activity activities={activities} isLoading={fetchingActivities} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,33 +1,38 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
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 { useParams } from 'react-router-dom';
|
||||||
|
import { Environment, EnvironmentVariable } from 'gql-client';
|
||||||
|
|
||||||
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
|
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 DisplayEnvironmentVariables from 'components/projects/project/settings/DisplayEnvironmentVariables';
|
||||||
import { useGQLClient } from 'context/GQLClientContext';
|
import { useGQLClient } from '../../../../../context/GQLClientContext';
|
||||||
import { EnvironmentVariablesFormValues } from '../../../../../types';
|
import { EnvironmentVariablesFormValues } from '../../../../../types';
|
||||||
import HorizontalLine from 'components/HorizontalLine';
|
import HorizontalLine from 'components/HorizontalLine';
|
||||||
import { Heading } from 'components/shared/Heading';
|
import { Heading } from 'components/shared/Heading';
|
||||||
|
import { Button } from 'components/shared/Button';
|
||||||
|
import { Checkbox } from 'components/shared/Checkbox';
|
||||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||||
|
import { InlineNotification } from 'components/shared/InlineNotification';
|
||||||
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
|
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
|
||||||
import { useToast } from 'components/shared/Toast';
|
|
||||||
import { Environment, EnvironmentVariable } from 'gql-client';
|
|
||||||
import EnvironmentVariablesForm from './EnvironmentVariablesForm';
|
|
||||||
import { FieldValues, FormProvider, useForm } from 'react-hook-form';
|
|
||||||
import { Button } from 'components/shared';
|
|
||||||
|
|
||||||
export const EnvironmentVariablesTabPanel = () => {
|
export const EnvironmentVariablesTabPanel = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
const { toast, dismiss } = useToast();
|
|
||||||
|
|
||||||
const [environmentVariables, setEnvironmentVariables] = useState<
|
const [environmentVariables, setEnvironmentVariables] = useState<
|
||||||
EnvironmentVariable[]
|
EnvironmentVariable[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const [createNewVariable, setCreateNewVariable] = useState(false);
|
const {
|
||||||
|
handleSubmit,
|
||||||
const methods = useForm<EnvironmentVariablesFormValues>({
|
register,
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitSuccessful, errors },
|
||||||
|
} = useForm<EnvironmentVariablesFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
variables: [{ key: '', value: '' }],
|
variables: [{ key: '', value: '' }],
|
||||||
environment: {
|
environment: {
|
||||||
@ -37,6 +42,21 @@ export const EnvironmentVariablesTabPanel = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const [createNewVariable, setCreateNewVariable] = useState(false);
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
name: 'variables',
|
||||||
|
control,
|
||||||
|
rules: {
|
||||||
|
required: 'Add at least 1 environment variables',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSubmitSuccessful) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}, [isSubmitSuccessful, reset, id]);
|
||||||
|
|
||||||
const getEnvironmentVariables = useCallback(
|
const getEnvironmentVariables = useCallback(
|
||||||
(environment: Environment) => {
|
(environment: Environment) => {
|
||||||
@ -47,6 +67,21 @@ export const EnvironmentVariablesTabPanel = () => {
|
|||||||
[environmentVariables, id],
|
[environmentVariables, id],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFieldEmpty = useMemo(() => {
|
||||||
|
if (errors.variables) {
|
||||||
|
return fields.some((_, index) => {
|
||||||
|
if (
|
||||||
|
errors.variables![index]?.value?.type === 'required' ||
|
||||||
|
errors.variables![index]?.key?.type === 'required'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}, [fields, errors.variables, id]);
|
||||||
|
|
||||||
const fetchEnvironmentVariables = useCallback(
|
const fetchEnvironmentVariables = useCallback(
|
||||||
async (id: string | undefined) => {
|
async (id: string | undefined) => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@ -63,47 +98,33 @@ export const EnvironmentVariablesTabPanel = () => {
|
|||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
const createEnvironmentVariablesHandler = useCallback(
|
const createEnvironmentVariablesHandler = useCallback(
|
||||||
async (createFormData: FieldValues) => {
|
async (createFormData: EnvironmentVariablesFormValues) => {
|
||||||
const environmentVariables = createFormData.variables.map(
|
const environmentVariables = createFormData.variables.map((variable) => {
|
||||||
(variable: any) => {
|
return {
|
||||||
return {
|
key: variable.key,
|
||||||
key: variable.key,
|
value: variable.value,
|
||||||
value: variable.value,
|
environments: Object.entries(createFormData.environment)
|
||||||
environments: Object.entries(createFormData.environment)
|
.filter(([, value]) => value === true)
|
||||||
.filter(([, value]) => value === true)
|
.map(([key]) => key.charAt(0).toUpperCase() + key.slice(1)),
|
||||||
.map(([key]) => key.charAt(0).toUpperCase() + key.slice(1)),
|
};
|
||||||
};
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { addEnvironmentVariables: isEnvironmentVariablesAdded } =
|
const { addEnvironmentVariables: isEnvironmentVariablesAdded } =
|
||||||
await client.addEnvironmentVariables(id!, environmentVariables);
|
await client.addEnvironmentVariables(id!, environmentVariables);
|
||||||
|
|
||||||
if (isEnvironmentVariablesAdded) {
|
if (isEnvironmentVariablesAdded) {
|
||||||
methods.reset();
|
reset();
|
||||||
setCreateNewVariable((cur) => !cur);
|
setCreateNewVariable((cur) => !cur);
|
||||||
|
|
||||||
fetchEnvironmentVariables(id);
|
fetchEnvironmentVariables(id);
|
||||||
|
|
||||||
toast({
|
toast.success(
|
||||||
id:
|
createFormData.variables.length > 1
|
||||||
createFormData.variables.length > 1
|
? `${createFormData.variables.length} variables added`
|
||||||
? 'env_variable_added'
|
: `Variable added`,
|
||||||
: 'env_variables_added',
|
);
|
||||||
title:
|
|
||||||
createFormData.variables.length > 1
|
|
||||||
? `${createFormData.variables.length} variables added`
|
|
||||||
: `Variable added`,
|
|
||||||
variant: 'success',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast.error('Environment variables not added');
|
||||||
id: 'env_variables_not_added',
|
|
||||||
title: 'Environment variables not added',
|
|
||||||
variant: 'error',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[id, client],
|
[id, client],
|
||||||
@ -125,14 +146,59 @@ export const EnvironmentVariablesTabPanel = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Heading>
|
</Heading>
|
||||||
<Collapse open={createNewVariable}>
|
<Collapse open={createNewVariable}>
|
||||||
<FormProvider {...methods}>
|
<div className="p-4 bg-slate-100">
|
||||||
<form
|
<form onSubmit={handleSubmit(createEnvironmentVariablesHandler)}>
|
||||||
onSubmit={methods.handleSubmit((data) =>
|
{fields.map((field, index) => {
|
||||||
createEnvironmentVariablesHandler(data),
|
return (
|
||||||
|
<AddEnvironmentVariableRow
|
||||||
|
key={field.id}
|
||||||
|
index={index}
|
||||||
|
register={register}
|
||||||
|
onDelete={() => remove(index)}
|
||||||
|
isDeleteDisabled={fields.length === 1}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="flex gap-1 p-2">
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
onClick={() =>
|
||||||
|
append({
|
||||||
|
key: '',
|
||||||
|
value: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
+ Add variable
|
||||||
|
</Button>
|
||||||
|
{/* TODO: Implement import environment varible functionality */}
|
||||||
|
<Button size="md" disabled>
|
||||||
|
Import .env
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isFieldEmpty && (
|
||||||
|
<InlineNotification
|
||||||
|
title="Please ensure no fields are empty before saving."
|
||||||
|
variant="danger"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
>
|
<div className="flex gap-2 p-2">
|
||||||
<div className="p-4 bg-slate-100">
|
<Checkbox
|
||||||
<EnvironmentVariablesForm />
|
label="Production"
|
||||||
|
{...register(`environment.production`)}
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Preview"
|
||||||
|
{...register(`environment.preview`)}
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Development"
|
||||||
|
{...register(`environment.development`)}
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<Button size="md" type="submit">
|
<Button size="md" type="submit">
|
||||||
@ -140,7 +206,7 @@ export const EnvironmentVariablesTabPanel = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</div>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
|
||||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
|
||||||
|
|
||||||
// TODO: Use custom checkbox component
|
|
||||||
import { Checkbox } from '@snowballtools/material-tailwind-react-fork';
|
|
||||||
|
|
||||||
import { Button } from 'components/shared/Button';
|
|
||||||
import { InlineNotification } from 'components/shared/InlineNotification';
|
|
||||||
import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow';
|
|
||||||
import { EnvironmentVariablesFormValues } from 'types/types';
|
|
||||||
|
|
||||||
const EnvironmentVariablesForm = () => {
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
control,
|
|
||||||
reset,
|
|
||||||
formState: { isSubmitSuccessful, errors },
|
|
||||||
} = useFormContext<EnvironmentVariablesFormValues>();
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
|
||||||
name: 'variables',
|
|
||||||
control,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isSubmitSuccessful) {
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
}, [isSubmitSuccessful, reset]);
|
|
||||||
|
|
||||||
const isFieldEmpty = useMemo(() => {
|
|
||||||
if (errors.variables) {
|
|
||||||
return fields.some((_, index) => {
|
|
||||||
if (
|
|
||||||
errors.variables![index]?.value?.type === 'required' ||
|
|
||||||
errors.variables![index]?.key?.type === 'required'
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}, [fields, errors.variables]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<AddEnvironmentVariableRow
|
|
||||||
key={field.id}
|
|
||||||
index={index}
|
|
||||||
register={register}
|
|
||||||
onDelete={() => remove(index)}
|
|
||||||
isDeleteDisabled={fields.length === 0}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<div className="flex gap-1 p-2">
|
|
||||||
<Button size="md" onClick={() => append({ key: '', value: '' })}>
|
|
||||||
+ Add variable
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{isFieldEmpty && (
|
|
||||||
<InlineNotification
|
|
||||||
title="Please ensure no fields are empty before saving."
|
|
||||||
variant="danger"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="flex gap-2 p-2">
|
|
||||||
<Checkbox label="Production" {...register('environment.production')} />
|
|
||||||
<Checkbox label="Preview" {...register('environment.preview')} />
|
|
||||||
<Checkbox
|
|
||||||
label="Development"
|
|
||||||
{...register('environment.development')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EnvironmentVariablesForm;
|
|
@ -1,12 +1,12 @@
|
|||||||
|
import toast from 'react-hot-toast';
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
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 { Table } from 'components/shared/Table';
|
||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
import { InlineNotification } from 'components/shared/InlineNotification';
|
import { InlineNotification } from 'components/shared/InlineNotification';
|
||||||
import { ArrowRightCircleIcon } from 'components/shared/CustomIcon';
|
import { ArrowRightCircleIcon } from 'components/shared/CustomIcon';
|
||||||
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
|
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
|
||||||
import { useToast } from 'components/shared/Toast';
|
|
||||||
|
|
||||||
const Config = () => {
|
const Config = () => {
|
||||||
const { id, orgSlug } = useParams();
|
const { id, orgSlug } = useParams();
|
||||||
@ -14,26 +14,15 @@ const Config = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const primaryDomainName = searchParams.get('name');
|
const primaryDomainName = searchParams.get('name');
|
||||||
const { toast, dismiss } = useToast();
|
|
||||||
|
|
||||||
const handleSubmitDomain = async () => {
|
const handleSubmitDomain = async () => {
|
||||||
if (primaryDomainName === null) {
|
if (primaryDomainName === null) {
|
||||||
toast({
|
toast.error('Cannot resolve domain name');
|
||||||
id: 'unresolvable_domain_name',
|
|
||||||
title: 'Cannot resolve domain name',
|
|
||||||
variant: 'error',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === undefined) {
|
if (id === undefined) {
|
||||||
toast({
|
toast.error('Cannot find project');
|
||||||
id: 'domain_cannot_find_project',
|
|
||||||
title: 'Cannot find project',
|
|
||||||
variant: 'error',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,20 +31,10 @@ const Config = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
toast({
|
toast.success('Domain added successfully');
|
||||||
id: 'domain_added_successfully',
|
|
||||||
title: 'Domain added successfully',
|
|
||||||
variant: 'success',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
navigate(`/${orgSlug}/projects/${id}/settings/domains`);
|
navigate(`/${orgSlug}/projects/${id}/settings/domains`);
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast.error('Error adding domain');
|
||||||
id: 'generic_error_adding_domain',
|
|
||||||
title: 'Error adding domaint',
|
|
||||||
variant: 'error',
|
|
||||||
onDismiss: dismiss,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,
|
DomainStatus,
|
||||||
Domain,
|
Domain,
|
||||||
Environment,
|
Environment,
|
||||||
Permission,
|
|
||||||
} from 'gql-client';
|
} from 'gql-client';
|
||||||
|
|
||||||
export const user: User = {
|
export const user: User = {
|
||||||
@ -45,7 +44,7 @@ export const organization: Organization = {
|
|||||||
export const member: ProjectMember = {
|
export const member: ProjectMember = {
|
||||||
id: '1',
|
id: '1',
|
||||||
member: user,
|
member: user,
|
||||||
permissions: [Permission.Edit],
|
permissions: [],
|
||||||
isPending: false,
|
isPending: false,
|
||||||
createdAt: '2021-08-01T00:00:00.000Z',
|
createdAt: '2021-08-01T00:00:00.000Z',
|
||||||
updatedAt: '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 = {
|
export const domain0: Domain = {
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'domain.com',
|
name: 'Domain',
|
||||||
createdAt: '2021-08-01T00:00:00.000Z',
|
createdAt: '2021-08-01T00:00:00.000Z',
|
||||||
updatedAt: '2021-08-01T00:00:00.000Z',
|
updatedAt: '2021-08-01T00:00:00.000Z',
|
||||||
branch: 'Branch',
|
branch: 'Branch',
|
||||||
@ -79,16 +78,6 @@ export const domain0: Domain = {
|
|||||||
redirectTo: null,
|
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 = {
|
export const deployment0: Deployment = {
|
||||||
id: '1',
|
id: '1',
|
||||||
url: 'https://deployment.com',
|
url: 'https://deployment.com',
|
||||||
@ -102,15 +91,6 @@ export const deployment0: Deployment = {
|
|||||||
domain: domain0,
|
domain: domain0,
|
||||||
commitMessage: 'Commit Message',
|
commitMessage: 'Commit Message',
|
||||||
createdBy: user,
|
createdBy: user,
|
||||||
deployer: {
|
|
||||||
deployerApiUrl: 'https://webapp-deployer-api.example.com',
|
|
||||||
deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu',
|
|
||||||
deployerLrn: 'lrn://deployer.apps.snowballtools.com ',
|
|
||||||
minimumPayment: '1000alnt',
|
|
||||||
baseDomain: 'pwa.example.com',
|
|
||||||
},
|
|
||||||
applicationDeploymentRequestId:
|
|
||||||
'bafyreiaycvq6imoppnpwdve4smj6t6ql5svt5zl3x6rimu4qwyzgjorize',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const project: Project = {
|
export const project: Project = {
|
||||||
@ -128,20 +108,7 @@ export const project: Project = {
|
|||||||
organization: organization,
|
organization: organization,
|
||||||
template: 'Template',
|
template: 'Template',
|
||||||
members: [member],
|
members: [member],
|
||||||
auctionId: '7553538436710373822151221341b43f577e07b0525d083cc9b2de98890138a1',
|
|
||||||
deployers: [
|
|
||||||
{
|
|
||||||
deployerApiUrl: 'https://webapp-deployer-api.example.com',
|
|
||||||
deployerId: 'bafyreicrtgmkir4evvvysxdqxddf2ftdq2wrzuodgvwnxr4rmubi4obdfu',
|
|
||||||
deployerLrn: 'lrn://deployer.apps.snowballtools.com ',
|
|
||||||
minimumPayment: '1000alnt',
|
|
||||||
baseDomain: 'pwa.example.com',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
paymentAddress: '0x657868687686rb4787987br8497298r79284797487',
|
|
||||||
txHash: '74btygeuydguygf838gcergurcbhuedbcjhu',
|
|
||||||
webhooks: ['beepboop'],
|
webhooks: ['beepboop'],
|
||||||
icon: 'Icon',
|
icon: 'Icon',
|
||||||
fundsReleased: true,
|
subDomain: 'SubDomain',
|
||||||
baseDomains: ['baseDomain'],
|
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ const meta: Meta<typeof AddEnvironmentVariableRow> = {
|
|||||||
pathParams: { userId: 'me' },
|
pathParams: { userId: 'me' },
|
||||||
},
|
},
|
||||||
routing: {
|
routing: {
|
||||||
path: '/deploy-tools/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings',
|
path: '/snowball-tools-1/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -17,7 +17,7 @@ const meta: Meta<typeof Config> = {
|
|||||||
pathParams: { userId: 'me' },
|
pathParams: { userId: 'me' },
|
||||||
},
|
},
|
||||||
routing: {
|
routing: {
|
||||||
path: '/deploy-tools/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings/domains/add/config',
|
path: '/snowball-tools-1/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings/domains/add/config',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -18,7 +18,7 @@ const meta: Meta<typeof DeleteProjectDialog> = {
|
|||||||
pathParams: { userId: 'me' },
|
pathParams: { userId: 'me' },
|
||||||
},
|
},
|
||||||
routing: {
|
routing: {
|
||||||
path: '/deploy-tools/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings',
|
path: '/snowball-tools-1/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -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 = {};
|
|
@ -17,7 +17,7 @@ const meta: Meta<typeof SetupDomain> = {
|
|||||||
pathParams: { userId: 'me' },
|
pathParams: { userId: 'me' },
|
||||||
},
|
},
|
||||||
routing: {
|
routing: {
|
||||||
path: '/deploy-tools/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings/domains',
|
path: '/snowball-tools-1/projects/6bb3bec2-d71b-4fc0-9e32-4767f68668f4/settings/domains',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
@ -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',
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,9 +1,9 @@
|
|||||||
import { BASE_URL } from './constants';
|
import { baseUrl } from './constants';
|
||||||
|
|
||||||
export async function verifyAccessCode(
|
export async function verifyAccessCode(
|
||||||
accesscode: string,
|
accesscode: string,
|
||||||
): Promise<boolean | null> {
|
): Promise<boolean | null> {
|
||||||
const res = await fetch(`${BASE_URL}/auth/accesscode`, {
|
const res = await fetch(`${baseUrl}/auth/accesscode`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
accesscode,
|
accesscode,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const BASE_URL = import.meta.env.VITE_SERVER_URL;
|
export const baseUrl = import.meta.env.VITE_SERVER_URL;
|
||||||
export const PASSKEY_WALLET_RPID = import.meta.env.VITE_PASSKEY_WALLET_RPID!;
|
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 TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!;
|
||||||
export const VITE_GITHUB_PWA_TEMPLATE_REPO = import.meta.env
|
export const VITE_GITHUB_PWA_TEMPLATE_REPO = import.meta.env
|
||||||
@ -9,4 +9,3 @@ 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_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_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;
|
export const VITE_LIT_RELAY_API_KEY = import.meta.env.VITE_LIT_RELAY_API_KEY;
|
||||||
export const VITE_LACONICD_CHAIN_ID = import.meta.env.VITE_LACONICD_CHAIN_ID;
|
|
||||||
|
@ -1,36 +1,34 @@
|
|||||||
// import React from 'react';
|
import React from 'react';
|
||||||
// import Bugsnag from '@bugsnag/js';
|
import Bugsnag from '@bugsnag/js';
|
||||||
// import BugsnagPluginReact from '@bugsnag/plugin-react';
|
import BugsnagPluginReact from '@bugsnag/plugin-react';
|
||||||
// import BugsnagPerformance from '@bugsnag/browser-performance';
|
import BugsnagPerformance from '@bugsnag/browser-performance';
|
||||||
|
|
||||||
// import { VITE_BUGSNAG_API_KEY } from './constants';
|
import { VITE_BUGSNAG_API_KEY } from './constants';
|
||||||
|
|
||||||
// if (VITE_BUGSNAG_API_KEY) {
|
if (VITE_BUGSNAG_API_KEY) {
|
||||||
// Bugsnag.start({
|
Bugsnag.start({
|
||||||
// apiKey: VITE_BUGSNAG_API_KEY,
|
apiKey: VITE_BUGSNAG_API_KEY,
|
||||||
// plugins: [new BugsnagPluginReact()],
|
plugins: [new BugsnagPluginReact()],
|
||||||
// });
|
});
|
||||||
// BugsnagPerformance.start({ apiKey: VITE_BUGSNAG_API_KEY });
|
BugsnagPerformance.start({ apiKey: VITE_BUGSNAG_API_KEY });
|
||||||
// }
|
}
|
||||||
|
|
||||||
// export const errorLoggingEnabled = !!VITE_BUGSNAG_API_KEY;
|
export const errorLoggingEnabled = !!VITE_BUGSNAG_API_KEY;
|
||||||
|
|
||||||
// export const LogErrorBoundary = VITE_BUGSNAG_API_KEY
|
export const LogErrorBoundary = VITE_BUGSNAG_API_KEY
|
||||||
// ? Bugsnag.getPlugin('react')!.createErrorBoundary(React)
|
? Bugsnag.getPlugin('react')!.createErrorBoundary(React)
|
||||||
// : ({ children }: any) => children;
|
: ({ children }: any) => children;
|
||||||
|
|
||||||
// export function logError(error: Error) {
|
export function logError(error: Error) {
|
||||||
// let errors: any[] = [error];
|
let errors: any[] = [error];
|
||||||
// let safety = 0;
|
let safety = 0;
|
||||||
// while (errors[errors.length - 1].cause && safety < 10) {
|
while (errors[errors.length - 1].cause && safety < 10) {
|
||||||
// errors.push('::caused by::', errors[errors.length - 1].cause);
|
errors.push('::caused by::', errors[errors.length - 1].cause);
|
||||||
// safety += 1;
|
safety += 1;
|
||||||
// }
|
}
|
||||||
// console.error(...errors);
|
console.error(...errors);
|
||||||
|
|
||||||
// if (VITE_BUGSNAG_API_KEY) {
|
if (VITE_BUGSNAG_API_KEY) {
|
||||||
// Bugsnag.notify(error);
|
Bugsnag.notify(error);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
export const LogErrorBoundary = ({ children }: any) => children;
|
|
||||||
|
48
packages/frontend/src/utils/siwe.ts
Normal file
48
packages/frontend/src/utils/siwe.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
export async function signInWithEthereum(
|
||||||
|
chainId: number,
|
||||||
|
action: 'signup' | 'login',
|
||||||
|
wallet: PKPEthersWallet,
|
||||||
|
) {
|
||||||
|
const message = await createSiweMessage(
|
||||||
|
chainId,
|
||||||
|
await wallet.getAddress(),
|
||||||
|
'Sign in with Ethereum to the app.',
|
||||||
|
);
|
||||||
|
const signature = await wallet.signMessage(message);
|
||||||
|
|
||||||
|
const res = await fetch(`${baseUrl}/auth/validate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ action, message, signature }),
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
return (await res.json()) as { success: boolean; error?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSiweMessage(
|
||||||
|
chainId: number,
|
||||||
|
address: string,
|
||||||
|
statement: string,
|
||||||
|
) {
|
||||||
|
const message = new SiweMessage({
|
||||||
|
domain,
|
||||||
|
address,
|
||||||
|
statement,
|
||||||
|
uri: origin,
|
||||||
|
version: '1',
|
||||||
|
chainId,
|
||||||
|
nonce: uuid().replace(/[^a-z0-9]/g, ''),
|
||||||
|
});
|
||||||
|
return message.prepareMessage();
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user