forked from cerc-io/snowballtools-base
Compare commits
1 Commits
main
...
06-24-Refa
Author | SHA1 | Date | |
---|---|---|---|
|
e88f4a9ab0 |
@ -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_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo'
|
||||
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_ALCHEMY_API_KEY = 'LACONIC_HOSTED_CONFIG_aplchemy_api_key'
|
||||
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'
|
||||
VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid'
|
||||
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
|
||||
|
||||
yarn || exit 1
|
||||
yarn build --ignore backend || exit 1
|
||||
yarn build || exit 1
|
||||
|
||||
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
||||
echo "Missing output directory: $OUTPUT_DIR" 1>&2
|
||||
|
@ -22,10 +22,10 @@ yarn build --ignore frontend
|
||||
|
||||
#### Local
|
||||
|
||||
Copy the `environments/local.toml.example` file to `environments/local.toml`:
|
||||
Copy the `envionments/local.toml.example` file to `envionments/local.toml`:
|
||||
|
||||
```zsh
|
||||
cp environments/local.toml.example environments/local.toml
|
||||
cp envionments/local.toml.example envionments/local.toml
|
||||
```
|
||||
|
||||
#### Staging environment variables
|
||||
@ -44,7 +44,7 @@ yarn start
|
||||
|
||||
## Deployment
|
||||
|
||||
Clone the [deployer repository](https://git.vdb.to/cerc-io/snowballtools-base-api-deployments):
|
||||
Clone the deployer repository:
|
||||
|
||||
```zsh
|
||||
git clone git@git.vdb.to:cerc-io/snowballtools-base-api-deployments.git
|
||||
|
@ -3,12 +3,9 @@
|
||||
port = 8000
|
||||
gqlPath = "/graphql"
|
||||
[server.session]
|
||||
secret = ""
|
||||
# Frontend webapp URL origin
|
||||
secret = "p4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi"
|
||||
appOriginUrl = "http://localhost:3000"
|
||||
# Set to true if server running behind proxy
|
||||
trustProxy = false
|
||||
# Backend URL hostname
|
||||
domain = "localhost"
|
||||
|
||||
[database]
|
||||
@ -22,7 +19,6 @@
|
||||
|
||||
[registryConfig]
|
||||
fetchDeploymentRecordDelay = 5000
|
||||
checkAuctionStatusDelay = 5000
|
||||
restEndpoint = "http://localhost:1317"
|
||||
gqlEndpoint = "http://localhost:9473/api"
|
||||
chainId = "laconic_9000-1"
|
||||
@ -30,14 +26,9 @@
|
||||
bondId = ""
|
||||
authority = ""
|
||||
[registryConfig.fee]
|
||||
gas = ""
|
||||
fees = ""
|
||||
gasPrice = "1alnt"
|
||||
amount = "200000"
|
||||
denom = "aphoton"
|
||||
gas = "750000"
|
||||
|
||||
# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions
|
||||
[auction]
|
||||
commitFee = "100000"
|
||||
commitsDuration = "120s"
|
||||
revealFee = "100000"
|
||||
revealsDuration = "120s"
|
||||
denom = "alnt"
|
||||
[misc]
|
||||
projectDomain = "apps.snowballtools.com"
|
||||
|
@ -4,10 +4,10 @@
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@cerc-io/registry-sdk": "^0.2.11",
|
||||
"@graphql-tools/schema": "^10.0.2",
|
||||
"@graphql-tools/utils": "^10.0.12",
|
||||
"@octokit/oauth-app": "^6.1.0",
|
||||
"@snowballtools/laconic-sdk": "^0.1.17",
|
||||
"@turnkey/sdk-server": "^0.1.0",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/express": "^4.17.21",
|
||||
|
@ -34,21 +34,16 @@ export interface RegistryConfig {
|
||||
privateKey: string;
|
||||
bondId: string;
|
||||
fetchDeploymentRecordDelay: number;
|
||||
checkAuctionStatusDelay: number;
|
||||
authority: string;
|
||||
fee: {
|
||||
amount: string;
|
||||
denom: string;
|
||||
gas: string;
|
||||
fees: string;
|
||||
gasPrice: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuctionConfig {
|
||||
commitFee: string;
|
||||
commitsDuration: string;
|
||||
revealFee: string;
|
||||
revealsDuration: string;
|
||||
denom: string;
|
||||
export interface MiscConfig {
|
||||
projectDomain: string;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
@ -56,7 +51,7 @@ export interface Config {
|
||||
database: DatabaseConfig;
|
||||
gitHub: GitHubConfig;
|
||||
registryConfig: RegistryConfig;
|
||||
auction: AuctionConfig;
|
||||
misc: MiscConfig;
|
||||
turnkey: {
|
||||
apiBaseUrl: string;
|
||||
apiPublicKey: string;
|
||||
|
@ -3,9 +3,7 @@ import {
|
||||
DeepPartial,
|
||||
FindManyOptions,
|
||||
FindOneOptions,
|
||||
FindOptionsWhere,
|
||||
IsNull,
|
||||
Not
|
||||
FindOptionsWhere
|
||||
} from 'typeorm';
|
||||
import path from 'path';
|
||||
import debug from 'debug';
|
||||
@ -13,7 +11,7 @@ import assert from 'assert';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { lowercase, numbers } from 'nanoid-dictionary';
|
||||
|
||||
import { DatabaseConfig } from './config';
|
||||
import { DatabaseConfig, MiscConfig } from './config';
|
||||
import { User } from './entity/User';
|
||||
import { Organization } from './entity/Organization';
|
||||
import { Project } from './entity/Project';
|
||||
@ -23,7 +21,6 @@ import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||
import { Domain } from './entity/Domain';
|
||||
import { getEntities, loadAndSaveData } from './utils';
|
||||
import { UserOrganization } from './entity/UserOrganization';
|
||||
import { Deployer } from './entity/Deployer';
|
||||
|
||||
const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json';
|
||||
|
||||
@ -34,8 +31,9 @@ const nanoid = customAlphabet(lowercase + numbers, 8);
|
||||
// TODO: Fix order of methods
|
||||
export class Database {
|
||||
private dataSource: DataSource;
|
||||
private projectDomain: string;
|
||||
|
||||
constructor({ dbPath }: DatabaseConfig) {
|
||||
constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) {
|
||||
this.dataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: dbPath,
|
||||
@ -43,9 +41,11 @@ export class Database {
|
||||
synchronize: true,
|
||||
logging: false
|
||||
});
|
||||
|
||||
this.projectDomain = projectDomain;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
async init (): Promise<void> {
|
||||
await this.dataSource.initialize();
|
||||
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 user = await userRepository.findOne(options);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async addUser(data: DeepPartial<User>): Promise<User> {
|
||||
async addUser (data: DeepPartial<User>): Promise<User> {
|
||||
const userRepository = this.dataSource.getRepository(User);
|
||||
const user = await userRepository.save(data);
|
||||
|
||||
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 updateResult = await userRepository.update({ id: user.id }, data);
|
||||
assert(updateResult.affected);
|
||||
@ -80,7 +80,7 @@ export class Database {
|
||||
return updateResult.affected > 0;
|
||||
}
|
||||
|
||||
async getOrganizations(
|
||||
async getOrganizations (
|
||||
options: FindManyOptions<Organization>
|
||||
): Promise<Organization[]> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||
@ -89,7 +89,7 @@ export class Database {
|
||||
return organizations;
|
||||
}
|
||||
|
||||
async getOrganization(
|
||||
async getOrganization (
|
||||
options: FindOneOptions<Organization>
|
||||
): Promise<Organization | null> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||
@ -98,7 +98,7 @@ export class Database {
|
||||
return organization;
|
||||
}
|
||||
|
||||
async getOrganizationsByUserId(userId: string): Promise<Organization[]> {
|
||||
async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
|
||||
const organizationRepository = this.dataSource.getRepository(Organization);
|
||||
|
||||
const userOrgs = await organizationRepository.find({
|
||||
@ -114,21 +114,21 @@ export class Database {
|
||||
return userOrgs;
|
||||
}
|
||||
|
||||
async addUserOrganization(data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
|
||||
async addUserOrganization (data: DeepPartial<UserOrganization>): Promise<UserOrganization> {
|
||||
const userOrganizationRepository = this.dataSource.getRepository(UserOrganization);
|
||||
const newUserOrganization = await userOrganizationRepository.save(data);
|
||||
|
||||
return newUserOrganization;
|
||||
}
|
||||
|
||||
async getProjects(options: FindManyOptions<Project>): Promise<Project[]> {
|
||||
async getProjects (options: FindManyOptions<Project>): Promise<Project[]> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
const projects = await projectRepository.find(options);
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
async getProjectById(projectId: string): Promise<Project | null> {
|
||||
async getProjectById (projectId: string): Promise<Project | null> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
|
||||
const project = await projectRepository
|
||||
@ -140,9 +140,7 @@ export class Database {
|
||||
)
|
||||
.leftJoinAndSelect('deployments.createdBy', 'user')
|
||||
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||
.leftJoinAndSelect('deployments.deployer', 'deployer')
|
||||
.leftJoinAndSelect('project.owner', 'owner')
|
||||
.leftJoinAndSelect('project.deployers', 'deployers')
|
||||
.leftJoinAndSelect('project.organization', 'organization')
|
||||
.where('project.id = :projectId', {
|
||||
projectId
|
||||
@ -152,25 +150,7 @@ export class Database {
|
||||
return project;
|
||||
}
|
||||
|
||||
async allProjectsWithoutDeployments(): Promise<Project[]> {
|
||||
const allProjects = await this.getProjects({
|
||||
where: {
|
||||
auctionId: Not(IsNull()),
|
||||
},
|
||||
relations: ['deployments'],
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const projects = allProjects.filter(project => {
|
||||
if (project.deletedAt !== null) return false;
|
||||
|
||||
return project.deployments.length === 0;
|
||||
});
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
async getProjectsInOrganization(
|
||||
async getProjectsInOrganization (
|
||||
userId: string,
|
||||
organizationSlug: string
|
||||
): Promise<Project[]> {
|
||||
@ -201,7 +181,7 @@ export class Database {
|
||||
/**
|
||||
* Get deployments with specified filter
|
||||
*/
|
||||
async getDeployments(
|
||||
async getDeployments (
|
||||
options: FindManyOptions<Deployment>
|
||||
): Promise<Deployment[]> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
@ -210,13 +190,12 @@ export class Database {
|
||||
return deployments;
|
||||
}
|
||||
|
||||
async getDeploymentsByProjectId(projectId: string): Promise<Deployment[]> {
|
||||
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
|
||||
return this.getDeployments({
|
||||
relations: {
|
||||
project: true,
|
||||
domain: true,
|
||||
createdBy: true,
|
||||
deployer: true,
|
||||
createdBy: true
|
||||
},
|
||||
where: {
|
||||
project: {
|
||||
@ -229,7 +208,7 @@ export class Database {
|
||||
});
|
||||
}
|
||||
|
||||
async getDeployment(
|
||||
async getDeployment (
|
||||
options: FindOneOptions<Deployment>
|
||||
): Promise<Deployment | null> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
@ -238,14 +217,14 @@ export class Database {
|
||||
return deployment;
|
||||
}
|
||||
|
||||
async getDomains(options: FindManyOptions<Domain>): Promise<Domain[]> {
|
||||
async getDomains (options: FindManyOptions<Domain>): Promise<Domain[]> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
const domains = await domainRepository.find(options);
|
||||
|
||||
return domains;
|
||||
}
|
||||
|
||||
async addDeployment(data: DeepPartial<Deployment>): Promise<Deployment> {
|
||||
async addDeployment (data: DeepPartial<Deployment>): Promise<Deployment> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
|
||||
const id = nanoid();
|
||||
@ -259,7 +238,7 @@ export class Database {
|
||||
return deployment;
|
||||
}
|
||||
|
||||
async getProjectMembersByProjectId(
|
||||
async getProjectMembersByProjectId (
|
||||
projectId: string
|
||||
): Promise<ProjectMember[]> {
|
||||
const projectMemberRepository =
|
||||
@ -280,7 +259,7 @@ export class Database {
|
||||
return projectMembers;
|
||||
}
|
||||
|
||||
async getEnvironmentVariablesByProjectId(
|
||||
async getEnvironmentVariablesByProjectId (
|
||||
projectId: string,
|
||||
filter?: FindOptionsWhere<EnvironmentVariable>
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
@ -299,7 +278,7 @@ export class Database {
|
||||
return environmentVariables;
|
||||
}
|
||||
|
||||
async removeProjectMemberById(projectMemberId: string): Promise<boolean> {
|
||||
async removeProjectMemberById (projectMemberId: string): Promise<boolean> {
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
|
||||
@ -314,7 +293,7 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
async updateProjectMemberById(
|
||||
async updateProjectMemberById (
|
||||
projectMemberId: string,
|
||||
data: DeepPartial<ProjectMember>
|
||||
): Promise<boolean> {
|
||||
@ -328,7 +307,7 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async addProjectMember(
|
||||
async addProjectMember (
|
||||
data: DeepPartial<ProjectMember>
|
||||
): Promise<ProjectMember> {
|
||||
const projectMemberRepository =
|
||||
@ -338,7 +317,7 @@ export class Database {
|
||||
return newProjectMember;
|
||||
}
|
||||
|
||||
async addEnvironmentVariables(
|
||||
async addEnvironmentVariables (
|
||||
data: DeepPartial<EnvironmentVariable>[]
|
||||
): Promise<EnvironmentVariable[]> {
|
||||
const environmentVariableRepository =
|
||||
@ -349,7 +328,7 @@ export class Database {
|
||||
return savedEnvironmentVariables;
|
||||
}
|
||||
|
||||
async updateEnvironmentVariable(
|
||||
async updateEnvironmentVariable (
|
||||
environmentVariableId: string,
|
||||
data: DeepPartial<EnvironmentVariable>
|
||||
): Promise<boolean> {
|
||||
@ -363,7 +342,7 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async deleteEnvironmentVariable(
|
||||
async deleteEnvironmentVariable (
|
||||
environmentVariableId: string
|
||||
): Promise<boolean> {
|
||||
const environmentVariableRepository =
|
||||
@ -379,7 +358,7 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
async getProjectMemberById(projectMemberId: string): Promise<ProjectMember> {
|
||||
async getProjectMemberById (projectMemberId: string): Promise<ProjectMember> {
|
||||
const projectMemberRepository =
|
||||
this.dataSource.getRepository(ProjectMember);
|
||||
|
||||
@ -402,7 +381,7 @@ export class Database {
|
||||
return projectMemberWithProject[0];
|
||||
}
|
||||
|
||||
async getProjectsBySearchText(
|
||||
async getProjectsBySearchText (
|
||||
userId: string,
|
||||
searchText: string
|
||||
): Promise<Project[]> {
|
||||
@ -424,14 +403,14 @@ export class Database {
|
||||
return projects;
|
||||
}
|
||||
|
||||
async updateDeploymentById(
|
||||
async updateDeploymentById (
|
||||
deploymentId: string,
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
return this.updateDeployment({ id: deploymentId }, data);
|
||||
}
|
||||
|
||||
async updateDeployment(
|
||||
async updateDeployment (
|
||||
criteria: FindOptionsWhere<Deployment>,
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
@ -441,7 +420,7 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async updateDeploymentsByProjectIds(
|
||||
async updateDeploymentsByProjectIds (
|
||||
projectIds: string[],
|
||||
data: DeepPartial<Deployment>
|
||||
): Promise<boolean> {
|
||||
@ -457,7 +436,7 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async deleteDeploymentById(deploymentId: string): Promise<boolean> {
|
||||
async deleteDeploymentById (deploymentId: string): Promise<boolean> {
|
||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||
const deployment = await deploymentRepository.findOneOrFail({
|
||||
where: {
|
||||
@ -470,7 +449,7 @@ export class Database {
|
||||
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);
|
||||
|
||||
// TODO: Check if organization exists
|
||||
@ -486,16 +465,12 @@ export class Database {
|
||||
id: organizationId
|
||||
});
|
||||
|
||||
newProject.subDomain = `${newProject.name}.${this.projectDomain}`;
|
||||
|
||||
return projectRepository.save(newProject);
|
||||
}
|
||||
|
||||
async saveProject(project: Project): Promise<Project> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
|
||||
return projectRepository.save(project);
|
||||
}
|
||||
|
||||
async updateProjectById(
|
||||
async updateProjectById (
|
||||
projectId: string,
|
||||
data: DeepPartial<Project>
|
||||
): Promise<boolean> {
|
||||
@ -508,7 +483,7 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async deleteProjectById(projectId: string): Promise<boolean> {
|
||||
async deleteProjectById (projectId: string): Promise<boolean> {
|
||||
const projectRepository = this.dataSource.getRepository(Project);
|
||||
const project = await projectRepository.findOneOrFail({
|
||||
where: {
|
||||
@ -524,7 +499,7 @@ export class Database {
|
||||
return Boolean(deleteResult);
|
||||
}
|
||||
|
||||
async deleteDomainById(domainId: string): Promise<boolean> {
|
||||
async deleteDomainById (domainId: string): Promise<boolean> {
|
||||
const domainRepository = this.dataSource.getRepository(Domain);
|
||||
|
||||
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 newDomain = await domainRepository.save(data);
|
||||
|
||||
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 domain = await domainRepository.findOne(options);
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
async updateDomainById(
|
||||
async updateDomainById (
|
||||
domainId: string,
|
||||
data: DeepPartial<Domain>
|
||||
): Promise<boolean> {
|
||||
@ -560,7 +535,7 @@ export class Database {
|
||||
return Boolean(updateResult.affected);
|
||||
}
|
||||
|
||||
async getDomainsByProjectId(
|
||||
async getDomainsByProjectId (
|
||||
projectId: string,
|
||||
filter?: FindOptionsWhere<Domain>
|
||||
): Promise<Domain[]> {
|
||||
@ -580,24 +555,4 @@ export class Database {
|
||||
|
||||
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 { Domain } from './Domain';
|
||||
import { User } from './User';
|
||||
import { Deployer } from './Deployer';
|
||||
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types';
|
||||
|
||||
export enum Environment {
|
||||
@ -34,21 +33,21 @@ export interface ApplicationDeploymentRequest {
|
||||
version: string;
|
||||
name: string;
|
||||
application: string;
|
||||
lrn?: string;
|
||||
auction?: string;
|
||||
config: string;
|
||||
meta: string;
|
||||
payment?: string;
|
||||
}
|
||||
|
||||
export interface ApplicationDeploymentRemovalRequest {
|
||||
type: string;
|
||||
version: string;
|
||||
deployment: string;
|
||||
auction?: string;
|
||||
payment?: string;
|
||||
}
|
||||
|
||||
export interface ApplicationDeploymentRemovalRequest {
|
||||
type: string;
|
||||
version: string;
|
||||
deployment: string;
|
||||
}
|
||||
|
||||
export interface ApplicationRecord {
|
||||
type: string;
|
||||
@ -113,23 +112,19 @@ export class Deployment {
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
|
||||
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRemovalRequestId!: string | null;
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null;
|
||||
|
||||
|
||||
@Column('varchar', { nullable: true })
|
||||
applicationDeploymentRemovalRecordId!: string | null;
|
||||
|
||||
@Column('simple-json', { nullable: true })
|
||||
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
|
||||
|
||||
@ManyToOne(() => Deployer)
|
||||
@JoinColumn({ name: 'deployerLrn' })
|
||||
deployer!: Deployer;
|
||||
|
||||
@Column({
|
||||
enum: Environment
|
||||
})
|
||||
@ -152,7 +147,7 @@ export class Deployment {
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Date | null;
|
||||
}
|
||||
|
@ -7,16 +7,13 @@ import {
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
DeleteDateColumn,
|
||||
JoinTable,
|
||||
ManyToMany
|
||||
DeleteDateColumn
|
||||
} from 'typeorm';
|
||||
|
||||
import { User } from './User';
|
||||
import { Organization } from './Organization';
|
||||
import { ProjectMember } from './ProjectMember';
|
||||
import { Deployment } from './Deployment';
|
||||
import { Deployer } from './Deployer';
|
||||
|
||||
@Entity()
|
||||
export class Project {
|
||||
@ -49,20 +46,6 @@ export class Project {
|
||||
@Column('text', { default: '' })
|
||||
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
|
||||
@Column('varchar', { nullable: true })
|
||||
template!: string | null;
|
||||
@ -70,10 +53,6 @@ export class Project {
|
||||
@Column('varchar', { nullable: true })
|
||||
framework!: string | null;
|
||||
|
||||
// Address of the user who created the project i.e. requested deployments
|
||||
@Column('varchar')
|
||||
paymentAddress!: string;
|
||||
|
||||
@Column({
|
||||
type: 'simple-array'
|
||||
})
|
||||
@ -82,6 +61,9 @@ export class Project {
|
||||
@Column('varchar')
|
||||
icon!: string;
|
||||
|
||||
@Column('varchar')
|
||||
subDomain!: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
|
@ -17,7 +17,7 @@ const log = debug('snowball:server');
|
||||
const OAUTH_CLIENT_TYPE = 'oauth-app';
|
||||
|
||||
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({
|
||||
clientType: OAUTH_CLIENT_TYPE,
|
||||
@ -25,7 +25,7 @@ export const main = async (): Promise<void> => {
|
||||
clientSecret: gitHub.oAuth.clientSecret,
|
||||
});
|
||||
|
||||
const db = new Database(database);
|
||||
const db = new Database(database, misc);
|
||||
await db.init();
|
||||
|
||||
const registry = new Registry(registryConfig);
|
||||
|
@ -1,12 +1,9 @@
|
||||
import assert from 'assert';
|
||||
import debug from 'debug';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Octokit } from 'octokit';
|
||||
import assert from 'assert';
|
||||
import { inc as semverInc } from 'semver';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { Account, DEFAULT_GAS_ESTIMATION_MULTIPLIER, Registry as LaconicRegistry, getGasPrice, parseGasAndFees } from '@cerc-io/registry-sdk';
|
||||
import { DeliverTxResponse, IndexedTx } from '@cosmjs/stargate';
|
||||
import { Registry as LaconicRegistry } from '@snowballtools/laconic-sdk';
|
||||
|
||||
import { RegistryConfig } from './config';
|
||||
import {
|
||||
@ -15,53 +12,49 @@ import {
|
||||
ApplicationDeploymentRequest,
|
||||
ApplicationDeploymentRemovalRequest
|
||||
} from './entity/Deployment';
|
||||
import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, DeployerRecord } from './types';
|
||||
import { getConfig, getRepoDetails, registryTransactionWithRetry, sleep } from './utils';
|
||||
import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types';
|
||||
import { sleep } from './utils';
|
||||
|
||||
const log = debug('snowball:registry');
|
||||
|
||||
const APP_RECORD_TYPE = 'ApplicationRecord';
|
||||
const APP_DEPLOYMENT_AUCTION_RECORD_TYPE = 'ApplicationDeploymentAuction';
|
||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
||||
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
|
||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
||||
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
|
||||
const WEBAPP_DEPLOYER_RECORD_TYPE = 'WebappDeployer'
|
||||
const SLEEP_DURATION = 1000;
|
||||
|
||||
// TODO: Move registry code to registry-sdk/watcher-ts
|
||||
// TODO: Move registry code to laconic-sdk/watcher-ts
|
||||
export class Registry {
|
||||
private registry: LaconicRegistry;
|
||||
private registryConfig: RegistryConfig;
|
||||
|
||||
constructor(registryConfig: RegistryConfig) {
|
||||
constructor (registryConfig: RegistryConfig) {
|
||||
this.registryConfig = registryConfig;
|
||||
|
||||
const gasPrice = getGasPrice(registryConfig.fee.gasPrice);
|
||||
|
||||
this.registry = new LaconicRegistry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId, gasPrice }
|
||||
registryConfig.chainId
|
||||
);
|
||||
}
|
||||
|
||||
async createApplicationRecord({
|
||||
octokit,
|
||||
repository,
|
||||
async createApplicationRecord ({
|
||||
appName,
|
||||
packageJSON,
|
||||
commitHash,
|
||||
appType,
|
||||
repoUrl
|
||||
}: {
|
||||
octokit: Octokit
|
||||
repository: string;
|
||||
appName: string;
|
||||
packageJSON: PackageJSON;
|
||||
commitHash: string;
|
||||
appType: string;
|
||||
repoUrl: string;
|
||||
}): Promise<{
|
||||
applicationRecordId: string;
|
||||
applicationRecordData: ApplicationRecord;
|
||||
}> {
|
||||
const { repo, repoUrl, packageJSON } = await getRepoDetails(octokit, repository, commitHash)
|
||||
// Use registry-sdk to publish record
|
||||
// Use laconic-sdk to publish record
|
||||
// Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts/publish-app-record.sh
|
||||
// Fetch previous records
|
||||
const records = await this.registry.queryRecords(
|
||||
@ -94,7 +87,7 @@ export class Registry {
|
||||
repository_ref: commitHash,
|
||||
repository: [repoUrl],
|
||||
app_type: appType,
|
||||
name: repo,
|
||||
name: appName,
|
||||
...(packageJSON.description && { description: packageJSON.description }),
|
||||
...(packageJSON.homepage && { homepage: packageJSON.homepage }),
|
||||
...(packageJSON.license && { license: packageJSON.license }),
|
||||
@ -107,158 +100,68 @@ export class Registry {
|
||||
...(packageJSON.version && { app_version: packageJSON.version })
|
||||
};
|
||||
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
|
||||
const result = await registryTransactionWithRetry(() =>
|
||||
this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationRecord,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
const result = await this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationRecord,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
log(`Published application record ${result.id}`);
|
||||
log('Application record data:', applicationRecord);
|
||||
|
||||
// TODO: Discuss computation of LRN
|
||||
const lrn = this.getLrn(repo);
|
||||
log(`Setting name: ${lrn} for record ID: ${result.id}`);
|
||||
// TODO: Discuss computation of CRN
|
||||
const crn = this.getCrn(appName);
|
||||
log(`Setting name: ${crn} for record ID: ${result.data.id}`);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn },
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn: `${lrn}@${applicationRecord.app_version}`
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
await this.registry.setName(
|
||||
{ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` },
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
await registryTransactionWithRetry(() =>
|
||||
this.registry.setName(
|
||||
{
|
||||
cid: result.id,
|
||||
lrn: `${lrn}@${applicationRecord.repository_ref}`
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
await this.registry.setName(
|
||||
{
|
||||
cid: result.data.id,
|
||||
crn: `${crn}@${applicationRecord.repository_ref}`
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
return {
|
||||
applicationRecordId: result.id,
|
||||
applicationRecordId: result.data.id,
|
||||
applicationRecordData: applicationRecord
|
||||
};
|
||||
}
|
||||
|
||||
async createApplicationDeploymentAuction(
|
||||
appName: string,
|
||||
octokit: Octokit,
|
||||
auctionParams: AuctionParams,
|
||||
data: DeepPartial<Deployment>,
|
||||
): Promise<{
|
||||
applicationDeploymentAuctionId: string;
|
||||
}> {
|
||||
assert(data.project?.repository, 'Project repository not found');
|
||||
|
||||
await this.createApplicationRecord({
|
||||
octokit,
|
||||
repository: data.project.repository,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
});
|
||||
|
||||
const lrn = this.getLrn(appName);
|
||||
const config = await getConfig();
|
||||
const auctionConfig = config.auction;
|
||||
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
const auctionResult = await registryTransactionWithRetry(() =>
|
||||
this.registry.createProviderAuction(
|
||||
{
|
||||
commitFee: auctionConfig.commitFee,
|
||||
commitsDuration: auctionConfig.commitsDuration,
|
||||
revealFee: auctionConfig.revealFee,
|
||||
revealsDuration: auctionConfig.revealsDuration,
|
||||
denom: auctionConfig.denom,
|
||||
maxPrice: auctionParams.maxPrice,
|
||||
numProviders: auctionParams.numProviders,
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
);
|
||||
|
||||
if (!auctionResult.auction) {
|
||||
throw new Error('Error creating auction');
|
||||
}
|
||||
|
||||
// Create record of type applicationDeploymentAuction and publish
|
||||
const applicationDeploymentAuction = {
|
||||
application: lrn,
|
||||
auction: auctionResult.auction.id,
|
||||
type: APP_DEPLOYMENT_AUCTION_RECORD_TYPE,
|
||||
};
|
||||
|
||||
const result = await registryTransactionWithRetry(() =>
|
||||
this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationDeploymentAuction,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
);
|
||||
|
||||
log(`Application deployment auction created: ${auctionResult.auction.id}`);
|
||||
log(`Application deployment auction record published: ${result.id}`);
|
||||
log('Application deployment auction data:', applicationDeploymentAuction);
|
||||
|
||||
return {
|
||||
applicationDeploymentAuctionId: auctionResult.auction.id,
|
||||
};
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRequest(data: {
|
||||
async createApplicationDeploymentRequest (data: {
|
||||
deployment: Deployment,
|
||||
appName: string,
|
||||
repository: string,
|
||||
auctionId?: string | null,
|
||||
lrn: string,
|
||||
environmentVariables: { [key: string]: string },
|
||||
dns: string,
|
||||
payment?: string | null
|
||||
}): Promise<{
|
||||
applicationDeploymentRequestId: string;
|
||||
applicationDeploymentRequestData: ApplicationDeploymentRequest;
|
||||
}> {
|
||||
const lrn = this.getLrn(data.appName);
|
||||
const records = await this.registry.resolveNames([lrn]);
|
||||
const crn = this.getCrn(data.appName);
|
||||
const records = await this.registry.resolveNames([crn]);
|
||||
const applicationRecord = records[0];
|
||||
|
||||
if (!applicationRecord) {
|
||||
throw new Error(`No record found for ${lrn}`);
|
||||
throw new Error(`No record found for ${crn}`);
|
||||
}
|
||||
|
||||
// Create record of type ApplicationDeploymentRequest and publish
|
||||
@ -266,9 +169,12 @@ export class Registry {
|
||||
type: APP_DEPLOYMENT_REQUEST_TYPE,
|
||||
version: '1.0.0',
|
||||
name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`,
|
||||
application: `${lrn}@${applicationRecord.attributes.app_version}`,
|
||||
application: `${crn}@${applicationRecord.attributes.app_version}`,
|
||||
dns: data.dns,
|
||||
|
||||
// TODO: Not set in test-progressive-web-app CI
|
||||
// deployment: '$CERC_REGISTRY_DEPLOYMENT_CRN',
|
||||
|
||||
// https://git.vdb.to/cerc-io/laconic-registry-cli/commit/129019105dfb93bebcea02fde0ed64d0f8e5983b
|
||||
config: JSON.stringify({
|
||||
env: data.environmentVariables
|
||||
@ -279,87 +185,32 @@ export class Registry {
|
||||
)}`,
|
||||
repository: data.repository,
|
||||
repository_ref: data.deployment.commitHash
|
||||
}),
|
||||
deployer: data.lrn,
|
||||
...(data.auctionId && { auction: data.auctionId }),
|
||||
...(data.payment && { payment: data.payment }),
|
||||
})
|
||||
};
|
||||
|
||||
await sleep(SLEEP_DURATION);
|
||||
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
|
||||
const result = await registryTransactionWithRetry(() =>
|
||||
this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationDeploymentRequest,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
const result = await this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationDeploymentRequest,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
log(`Application deployment request record published: ${result.id}`);
|
||||
log(`Application deployment request record published: ${result.data.id}`);
|
||||
log('Application deployment request data:', applicationDeploymentRequest);
|
||||
|
||||
return {
|
||||
applicationDeploymentRequestId: result.id,
|
||||
applicationDeploymentRequestId: result.data.id,
|
||||
applicationDeploymentRequestData: applicationDeploymentRequest
|
||||
};
|
||||
}
|
||||
|
||||
async 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
|
||||
*/
|
||||
async getDeploymentRecords(
|
||||
async getDeploymentRecords (
|
||||
deployments: Deployment[]
|
||||
): Promise<AppDeploymentRecord[]> {
|
||||
// Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments
|
||||
@ -371,33 +222,20 @@ export class Registry {
|
||||
true
|
||||
);
|
||||
|
||||
// Filter records with ApplicationDeploymentRequestId ID and Deployment specific URL
|
||||
// Filter records with ApplicationRecord ID and Deployment specific URL
|
||||
return records.filter((record: AppDeploymentRecord) =>
|
||||
deployments.some(
|
||||
(deployment) =>
|
||||
deployment.applicationDeploymentRequestId === record.attributes.request &&
|
||||
deployment.applicationRecordId === record.attributes.application &&
|
||||
record.attributes.url.includes(deployment.id)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
async getDeploymentRecordsByFilter(filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
||||
async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
||||
return this.registry.queryRecords(
|
||||
{
|
||||
type: APP_DEPLOYMENT_RECORD_TYPE,
|
||||
@ -410,7 +248,7 @@ export class Registry {
|
||||
/**
|
||||
* Fetch ApplicationDeploymentRemovalRecords for deployments
|
||||
*/
|
||||
async getDeploymentRemovalRecords(
|
||||
async getDeploymentRemovalRecords (
|
||||
deployments: Deployment[]
|
||||
): Promise<AppDeploymentRemovalRecord[]> {
|
||||
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
|
||||
@ -431,11 +269,8 @@ export class Registry {
|
||||
);
|
||||
}
|
||||
|
||||
async createApplicationDeploymentRemovalRequest(data: {
|
||||
async createApplicationDeploymentRemovalRequest (data: {
|
||||
deploymentId: string;
|
||||
deployerLrn: string;
|
||||
auctionId?: string | null;
|
||||
payment?: string | null;
|
||||
}): Promise<{
|
||||
applicationDeploymentRemovalRequestId: string;
|
||||
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
||||
@ -443,93 +278,30 @@ export class Registry {
|
||||
const applicationDeploymentRemovalRequest = {
|
||||
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
|
||||
version: '1.0.0',
|
||||
deployment: data.deploymentId,
|
||||
deployer: data.deployerLrn,
|
||||
...(data.auctionId && { auction: data.auctionId }),
|
||||
...(data.payment && { payment: data.payment }),
|
||||
deployment: data.deploymentId
|
||||
};
|
||||
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
|
||||
const result = await registryTransactionWithRetry(() =>
|
||||
this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationDeploymentRemovalRequest,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
this.registryConfig.privateKey,
|
||||
fee
|
||||
)
|
||||
const result = await this.registry.setRecord(
|
||||
{
|
||||
privateKey: this.registryConfig.privateKey,
|
||||
record: applicationDeploymentRemovalRequest,
|
||||
bondId: this.registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
this.registryConfig.fee
|
||||
);
|
||||
|
||||
log(`Application deployment removal request record published: ${result.id}`);
|
||||
log(`Application deployment removal request record published: ${result.data.id}`);
|
||||
log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
|
||||
|
||||
return {
|
||||
applicationDeploymentRemovalRequestId: result.id,
|
||||
applicationDeploymentRemovalRequestId: result.data.id,
|
||||
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
|
||||
};
|
||||
}
|
||||
|
||||
async getCompletedAuctionIds(auctionIds: string[]): Promise<string[]> {
|
||||
if (auctionIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const auctions = await this.registry.getAuctionsByIds(auctionIds);
|
||||
|
||||
const completedAuctions = auctions
|
||||
.filter((auction: { id: string, status: string }) => auction.status === 'completed')
|
||||
.map((auction: { id: string, status: string }) => auction.id);
|
||||
|
||||
return completedAuctions;
|
||||
}
|
||||
|
||||
async getRecordsByName(name: string): Promise<any> {
|
||||
return this.registry.resolveNames([name]);
|
||||
}
|
||||
|
||||
async getAuctionData(auctionId: string): Promise<any> {
|
||||
return this.registry.getAuctionsByIds([auctionId]);
|
||||
}
|
||||
|
||||
async sendTokensToAccount(receiverAddress: string, amount: string): Promise<DeliverTxResponse> {
|
||||
const fee = parseGasAndFees(this.registryConfig.fee.gas, this.registryConfig.fee.fees);
|
||||
const account = await this.getAccount();
|
||||
const laconicClient = await this.registry.getLaconicClient(account);
|
||||
const txResponse: DeliverTxResponse =
|
||||
await registryTransactionWithRetry(() =>
|
||||
laconicClient.sendTokens(account.address, receiverAddress,
|
||||
[
|
||||
{
|
||||
denom: 'alnt',
|
||||
amount
|
||||
}
|
||||
],
|
||||
fee || DEFAULT_GAS_ESTIMATION_MULTIPLIER)
|
||||
);
|
||||
|
||||
return txResponse;
|
||||
}
|
||||
|
||||
async getAccount(): Promise<Account> {
|
||||
const account = new Account(Buffer.from(this.registryConfig.privateKey, 'hex'));
|
||||
await account.init();
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
async getTxResponse(txHash: string): Promise<IndexedTx | null> {
|
||||
const account = await this.getAccount();
|
||||
const laconicClient = await this.registry.getLaconicClient(account);
|
||||
const txResponse: IndexedTx | null = await laconicClient.getTx(txHash);
|
||||
|
||||
return txResponse;
|
||||
}
|
||||
|
||||
getLrn(appName: string): string {
|
||||
getCrn (appName: string): string {
|
||||
assert(this.registryConfig.authority, "Authority doesn't exist");
|
||||
return `lrn://${this.registryConfig.authority}/applications/${appName}`;
|
||||
return `crn://${this.registryConfig.authority}/applications/${appName}`;
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { Permission } from './entity/ProjectMember';
|
||||
import { Domain } from './entity/Domain';
|
||||
import { Project } from './entity/Project';
|
||||
import { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||
import { AddProjectFromTemplateInput, AuctionParams, EnvironmentVariables } from './types';
|
||||
import { AddProjectFromTemplateInput } from './types';
|
||||
|
||||
const log = debug('snowball:resolver');
|
||||
|
||||
@ -22,8 +22,8 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
return service.getOrganizationsByUserId(context.user);
|
||||
},
|
||||
|
||||
project: async (_: any, { projectId }: { projectId: string }, context: any) => {
|
||||
return service.getProjectById(context.user, projectId);
|
||||
project: async (_: any, { projectId }: { projectId: string }) => {
|
||||
return service.getProjectById(projectId);
|
||||
},
|
||||
|
||||
projectsInOrganization: async (
|
||||
@ -69,32 +69,6 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
) => {
|
||||
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
|
||||
@ -229,16 +203,7 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
{
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
}: {
|
||||
organizationSlug: string;
|
||||
data: AddProjectFromTemplateInput;
|
||||
lrn: string;
|
||||
auctionParams: AuctionParams;
|
||||
environmentVariables: EnvironmentVariables[];
|
||||
},
|
||||
}: { organizationSlug: string; data: AddProjectFromTemplateInput },
|
||||
context: any,
|
||||
) => {
|
||||
try {
|
||||
@ -246,9 +211,6 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
context.user,
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
@ -261,27 +223,11 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
||||
{
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
}: {
|
||||
organizationSlug: string;
|
||||
data: DeepPartial<Project>;
|
||||
lrn: string;
|
||||
auctionParams: AuctionParams,
|
||||
environmentVariables: EnvironmentVariables[];
|
||||
},
|
||||
}: { organizationSlug: string; data: DeepPartial<Project> },
|
||||
context: any,
|
||||
) => {
|
||||
try {
|
||||
return await service.addProject(
|
||||
context.user,
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
);
|
||||
return await service.addProject(context.user, organizationSlug, data);
|
||||
} catch (err) {
|
||||
log(err);
|
||||
throw err;
|
||||
|
@ -5,6 +5,19 @@ import { authenticateUser, createUser } from '../turnkey-backend';
|
||||
|
||||
const router = Router();
|
||||
|
||||
//
|
||||
// Access Code
|
||||
//
|
||||
router.post('/accesscode', async (req, res) => {
|
||||
console.log('Access Code', req.body);
|
||||
const { accesscode } = req.body;
|
||||
if (accesscode === '44444') {
|
||||
return res.send({ isValid: true });
|
||||
} else {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Turnkey
|
||||
//
|
||||
@ -27,7 +40,7 @@ router.post('/register', async (req, res) => {
|
||||
userEmail: email,
|
||||
userName: email.split('@')[0],
|
||||
});
|
||||
req.session.address = user.id;
|
||||
req.session.userId = user.id;
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
@ -39,7 +52,7 @@ router.post('/authenticate', async (req, res) => {
|
||||
signedWhoamiRequest,
|
||||
);
|
||||
if (user) {
|
||||
req.session.address = user.id;
|
||||
req.session.userId = user.id;
|
||||
res.sendStatus(200);
|
||||
} else {
|
||||
res.sendStatus(401);
|
||||
@ -47,10 +60,11 @@ router.post('/authenticate', async (req, res) => {
|
||||
});
|
||||
|
||||
//
|
||||
// SIWE Auth
|
||||
// Lit
|
||||
//
|
||||
|
||||
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({
|
||||
signature,
|
||||
});
|
||||
@ -61,20 +75,23 @@ router.post('/validate', async (req, res) => {
|
||||
const service: Service = req.app.get('service');
|
||||
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({
|
||||
ethAddress: data.address,
|
||||
email: `${data.address}@example.com`,
|
||||
email: '',
|
||||
name: '',
|
||||
subOrgId: '',
|
||||
turnkeyWalletId: '',
|
||||
});
|
||||
|
||||
// SIWESession from the web3modal library requires both address and chain ID
|
||||
req.session.address = newUser.id;
|
||||
req.session.chainId = data.chainId;
|
||||
} else {
|
||||
req.session.address = user.id;
|
||||
req.session.chainId = data.chainId;
|
||||
req.session.userId = newUser.id;
|
||||
} else if (action === 'login') {
|
||||
if (!user) {
|
||||
return res.send({ success: false, error: 'user_not_found' });
|
||||
}
|
||||
req.session.userId = user.id;
|
||||
}
|
||||
|
||||
res.send({ success });
|
||||
@ -84,10 +101,9 @@ router.post('/validate', async (req, res) => {
|
||||
// General
|
||||
//
|
||||
router.get('/session', (req, res) => {
|
||||
if (req.session.address && req.session.chainId) {
|
||||
if (req.session.userId) {
|
||||
res.send({
|
||||
address: req.session.address,
|
||||
chainId: req.session.chainId
|
||||
userId: req.session.userId,
|
||||
});
|
||||
} else {
|
||||
res.status(401).send({ error: 'Unauthorized: No active session' });
|
||||
@ -95,12 +111,9 @@ router.get('/session', (req, res) => {
|
||||
});
|
||||
|
||||
router.post('/logout', (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.send({ success: false });
|
||||
}
|
||||
res.send({ success: true });
|
||||
});
|
||||
// This is how you clear cookie-session
|
||||
(req as any).session = null;
|
||||
res.send({ success: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
@ -3,7 +3,7 @@ import { Router } from 'express';
|
||||
const router = Router();
|
||||
|
||||
router.get('/version', async (req, res) => {
|
||||
return res.send({ version: '0.0.9' });
|
||||
return res.send({ version: '0.0.8' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
@ -22,13 +22,6 @@ enum DeploymentStatus {
|
||||
Deleting
|
||||
}
|
||||
|
||||
enum AuctionStatus {
|
||||
completed
|
||||
reveal
|
||||
commit
|
||||
expired
|
||||
}
|
||||
|
||||
enum DomainStatus {
|
||||
Live
|
||||
Pending
|
||||
@ -72,13 +65,8 @@ type Project {
|
||||
repository: String!
|
||||
prodBranch: String!
|
||||
description: String
|
||||
deployers: [Deployer!]
|
||||
auctionId: String
|
||||
fundsReleased: Boolean
|
||||
template: String
|
||||
framework: String
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
webhooks: [String!]
|
||||
members: [ProjectMember!]
|
||||
environmentVariables: [EnvironmentVariable!]
|
||||
@ -86,7 +74,7 @@ type Project {
|
||||
updatedAt: String!
|
||||
organization: Organization!
|
||||
icon: String
|
||||
baseDomains: [String!]
|
||||
subDomain: String
|
||||
}
|
||||
|
||||
type ProjectMember {
|
||||
@ -106,10 +94,7 @@ type Deployment {
|
||||
commitMessage: String!
|
||||
url: String
|
||||
environment: Environment!
|
||||
deployer: Deployer
|
||||
applicationDeploymentRequestId: String
|
||||
isCurrent: Boolean!
|
||||
baseDomain: String
|
||||
status: DeploymentStatus!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
@ -135,17 +120,6 @@ type EnvironmentVariable {
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Deployer {
|
||||
deployerLrn: String!
|
||||
deployerId: String!
|
||||
deployerApiUrl: String!
|
||||
minimumPayment: String
|
||||
paymentAddress: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
baseDomain: String
|
||||
}
|
||||
|
||||
type AuthResult {
|
||||
token: String!
|
||||
}
|
||||
@ -162,8 +136,6 @@ input AddProjectFromTemplateInput {
|
||||
owner: String!
|
||||
name: String!
|
||||
isPrivate: Boolean!
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
}
|
||||
|
||||
input AddProjectInput {
|
||||
@ -171,8 +143,6 @@ input AddProjectInput {
|
||||
repository: String!
|
||||
prodBranch: String!
|
||||
template: String
|
||||
paymentAddress: String!
|
||||
txHash: String!
|
||||
}
|
||||
|
||||
input UpdateProjectInput {
|
||||
@ -212,48 +182,6 @@ input FilterDomainsInput {
|
||||
status: DomainStatus
|
||||
}
|
||||
|
||||
type Fee {
|
||||
type: String!
|
||||
quantity: String!
|
||||
}
|
||||
|
||||
type Bid {
|
||||
auctionId: String!
|
||||
bidderAddress: String!
|
||||
status: String!
|
||||
commitHash: String!
|
||||
commitTime: String
|
||||
commitFee: Fee
|
||||
revealTime: String
|
||||
revealFee: Fee
|
||||
bidAmount: Fee
|
||||
}
|
||||
|
||||
type Auction {
|
||||
id: String!
|
||||
kind: String!
|
||||
status: String!
|
||||
ownerAddress: String!
|
||||
createTime: String!
|
||||
commitsEndTime: String!
|
||||
revealsEndTime: String!
|
||||
commitFee: Fee!
|
||||
revealFee: Fee!
|
||||
minimumBid: Fee
|
||||
winnerAddresses: [String!]!
|
||||
winnerBids: [Fee!]
|
||||
winnerPrice: Fee
|
||||
maxPrice: Fee
|
||||
numProviders: Int!
|
||||
fundsReleased: Boolean!
|
||||
bids: [Bid!]!
|
||||
}
|
||||
|
||||
input AuctionParams {
|
||||
maxPrice: String,
|
||||
numProviders: Int,
|
||||
}
|
||||
|
||||
type Query {
|
||||
user: User!
|
||||
organizations: [Organization!]
|
||||
@ -264,11 +192,7 @@ type Query {
|
||||
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
||||
projectMembers(projectId: String!): [ProjectMember!]
|
||||
searchProjects(searchText: String!): [Project!]
|
||||
getAuctionData(auctionId: String!): Auction!
|
||||
domains(projectId: String!, filter: FilterDomainsInput): [Domain]
|
||||
deployers: [Deployer]
|
||||
address: String!
|
||||
verifyTx(txHash: String!, amount: String!, senderAddress: String!): Boolean!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@ -291,17 +215,8 @@ type Mutation {
|
||||
addProjectFromTemplate(
|
||||
organizationSlug: String!
|
||||
data: AddProjectFromTemplateInput
|
||||
lrn: String
|
||||
auctionParams: AuctionParams
|
||||
environmentVariables: [AddEnvironmentVariableInput!]
|
||||
): Project!
|
||||
addProject(
|
||||
organizationSlug: String!
|
||||
data: AddProjectInput!
|
||||
lrn: String
|
||||
auctionParams: AuctionParams
|
||||
environmentVariables: [AddEnvironmentVariableInput!]
|
||||
): Project!
|
||||
addProject(organizationSlug: String!, data: AddProjectInput): Project!
|
||||
updateProject(projectId: String!, data: UpdateProjectInput): Boolean!
|
||||
redeployToProd(deploymentId: String!): Boolean!
|
||||
deleteProject(projectId: String!): Boolean!
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
ApolloServerPluginLandingPageLocalDefault,
|
||||
AuthenticationError,
|
||||
} from 'apollo-server-core';
|
||||
import session from 'express-session';
|
||||
import cookieSession from 'cookie-session';
|
||||
|
||||
import { TypeSource } from '@graphql-tools/utils';
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||
@ -22,13 +22,9 @@ import { Service } from './service';
|
||||
|
||||
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' {
|
||||
interface SessionData {
|
||||
address: string;
|
||||
chainId: number;
|
||||
userId: string;
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,13 +54,14 @@ export const createAndStartServer = async (
|
||||
context: async ({ req }) => {
|
||||
// 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');
|
||||
}
|
||||
|
||||
const user = await service.getUser(address);
|
||||
const user = await service.getUser(userId);
|
||||
|
||||
return { user };
|
||||
},
|
||||
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) {
|
||||
// trust first proxy
|
||||
app.set('trust proxy', 1);
|
||||
}
|
||||
|
||||
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({
|
||||
|
@ -1,12 +1,12 @@
|
||||
import assert from 'assert';
|
||||
import debug from 'debug';
|
||||
import { DeepPartial, FindOptionsWhere, IsNull, Not } from 'typeorm';
|
||||
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
||||
import { Octokit, RequestError } from 'octokit';
|
||||
|
||||
import { OAuthApp } from '@octokit/oauth-app';
|
||||
|
||||
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 { EnvironmentVariable } from './entity/EnvironmentVariable';
|
||||
import { Organization } from './entity/Organization';
|
||||
@ -14,19 +14,15 @@ import { Project } from './entity/Project';
|
||||
import { Permission, ProjectMember } from './entity/ProjectMember';
|
||||
import { User } from './entity/User';
|
||||
import { Registry } from './registry';
|
||||
import { Deployer } from './entity/Deployer';
|
||||
import { GitHubConfig, RegistryConfig } from './config';
|
||||
import {
|
||||
AddProjectFromTemplateInput,
|
||||
AppDeploymentRecord,
|
||||
AppDeploymentRemovalRecord,
|
||||
AuctionParams,
|
||||
DeployerRecord,
|
||||
EnvironmentVariables,
|
||||
GitPushEventPayload,
|
||||
PackageJSON,
|
||||
} from './types';
|
||||
import { Role } from './entity/UserOrganization';
|
||||
import { getRepoDetails } from './utils';
|
||||
|
||||
const log = debug('snowball:service');
|
||||
|
||||
@ -43,16 +39,15 @@ interface Config {
|
||||
export class Service {
|
||||
private db: Database;
|
||||
private oauthApp: OAuthApp;
|
||||
private laconicRegistry: Registry;
|
||||
private registry: Registry;
|
||||
private config: Config;
|
||||
|
||||
private deployRecordCheckTimeout?: NodeJS.Timeout;
|
||||
private auctionStatusCheckTimeout?: NodeJS.Timeout;
|
||||
|
||||
constructor(config: Config, db: Database, app: OAuthApp, registry: Registry) {
|
||||
this.db = db;
|
||||
this.oauthApp = app;
|
||||
this.laconicRegistry = registry;
|
||||
this.registry = registry;
|
||||
this.config = config;
|
||||
this.init();
|
||||
}
|
||||
@ -65,8 +60,6 @@ export class Service {
|
||||
this.checkDeployRecordsAndUpdate();
|
||||
// Start check for ApplicationDeploymentRemovalRecords asynchronously
|
||||
this.checkDeploymentRemovalRecordsAndUpdate();
|
||||
// Start check for Deployment Auctions asynchronously
|
||||
this.checkAuctionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -74,7 +67,6 @@ export class Service {
|
||||
*/
|
||||
destroy(): void {
|
||||
clearTimeout(this.deployRecordCheckTimeout);
|
||||
clearTimeout(this.auctionStatusCheckTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -116,7 +108,7 @@ export class Service {
|
||||
}
|
||||
|
||||
// Fetch ApplicationDeploymentRecord for deployments
|
||||
const records = await this.laconicRegistry.getDeploymentRecords(deployments);
|
||||
const records = await this.registry.getDeploymentRecords(deployments);
|
||||
log(`Found ${records.length} ApplicationDeploymentRecords`);
|
||||
|
||||
// Update deployments for which ApplicationDeploymentRecords were returned
|
||||
@ -149,7 +141,7 @@ export class Service {
|
||||
|
||||
// Fetch ApplicationDeploymentRemovalRecords for deployments
|
||||
const records =
|
||||
await this.laconicRegistry.getDeploymentRemovalRecords(deployments);
|
||||
await this.registry.getDeploymentRemovalRecords(deployments);
|
||||
log(`Found ${records.length} ApplicationDeploymentRemovalRecords`);
|
||||
|
||||
// Update deployments for which ApplicationDeploymentRemovalRecords were returned
|
||||
@ -165,28 +157,41 @@ export class Service {
|
||||
|
||||
/**
|
||||
* Update deployments with ApplicationDeploymentRecord data
|
||||
* Deployments that are completed but not updated in DB
|
||||
*/
|
||||
async updateDeploymentsWithRecordData(
|
||||
records: AppDeploymentRecord[],
|
||||
): Promise<void> {
|
||||
// Fetch the deployments to be updated using deployment requestId
|
||||
// Get deployments for ApplicationDeploymentRecords
|
||||
const deployments = await this.db.getDeployments({
|
||||
where: records.map((record) => ({
|
||||
applicationDeploymentRequestId: record.attributes.request,
|
||||
applicationRecordId: record.attributes.application,
|
||||
})),
|
||||
relations: {
|
||||
deployer: true,
|
||||
project: true,
|
||||
},
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
});
|
||||
|
||||
// Get project IDs of deployments that are in production environment
|
||||
const productionDeploymentProjectIds = deployments.reduce(
|
||||
(acc, deployment): Set<string> => {
|
||||
if (deployment.environment === Environment.Production) {
|
||||
acc.add(deployment.projectId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
new Set<string>(),
|
||||
);
|
||||
|
||||
// Set old deployments isCurrent to false
|
||||
await this.db.updateDeploymentsByProjectIds(
|
||||
Array.from(productionDeploymentProjectIds),
|
||||
{ isCurrent: false },
|
||||
);
|
||||
|
||||
const recordToDeploymentsMap = deployments.reduce(
|
||||
(acc: { [key: string]: Deployment }, deployment) => {
|
||||
acc[deployment.applicationDeploymentRequestId!] = deployment;
|
||||
acc[deployment.applicationRecordId] = deployment;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
@ -194,57 +199,22 @@ export class Service {
|
||||
|
||||
// Update deployment data for ApplicationDeploymentRecords
|
||||
const deploymentUpdatePromises = records.map(async (record) => {
|
||||
const deployment = recordToDeploymentsMap[record.attributes.request];
|
||||
const deployment = recordToDeploymentsMap[record.attributes.application];
|
||||
|
||||
if (!deployment.project) {
|
||||
log(`Project ${deployment.projectId} not found`);
|
||||
return;
|
||||
} else {
|
||||
deployment.applicationDeploymentRecordId = record.id;
|
||||
deployment.applicationDeploymentRecordData = record.attributes;
|
||||
deployment.url = record.attributes.url;
|
||||
deployment.status = DeploymentStatus.Ready;
|
||||
deployment.isCurrent = deployment.environment === Environment.Production;
|
||||
await this.db.updateDeploymentById(deployment.id, {
|
||||
applicationDeploymentRecordId: record.id,
|
||||
applicationDeploymentRecordData: record.attributes,
|
||||
url: record.attributes.url,
|
||||
status: DeploymentStatus.Ready,
|
||||
isCurrent: deployment.environment === Environment.Production,
|
||||
});
|
||||
|
||||
await this.db.updateDeploymentById(deployment.id, deployment);
|
||||
|
||||
// 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}`,
|
||||
);
|
||||
}
|
||||
log(
|
||||
`Updated deployment ${deployment.id} with URL ${record.attributes.url}`,
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
return this.db.getUser({
|
||||
where: {
|
||||
@ -364,7 +295,7 @@ export class Service {
|
||||
}
|
||||
|
||||
async createUser(params: {
|
||||
name?: string;
|
||||
name: string;
|
||||
email: string;
|
||||
subOrgId: string;
|
||||
ethAddress: string;
|
||||
@ -407,13 +338,8 @@ export class Service {
|
||||
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);
|
||||
|
||||
if (dbProject && dbProject.owner.id !== user.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dbProject;
|
||||
}
|
||||
|
||||
@ -593,7 +519,6 @@ export class Service {
|
||||
domain: prodBranchDomains[0],
|
||||
commitHash: oldDeployment.commitHash,
|
||||
commitMessage: oldDeployment.commitMessage,
|
||||
deployer: oldDeployment.deployer
|
||||
});
|
||||
|
||||
return newDeployment;
|
||||
@ -603,20 +528,44 @@ export class Service {
|
||||
userId: string,
|
||||
octokit: Octokit,
|
||||
data: DeepPartial<Deployment>,
|
||||
deployerLrn?: string
|
||||
): Promise<Deployment> {
|
||||
assert(data.project?.repository, 'Project repository not found');
|
||||
log(
|
||||
`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)
|
||||
const { applicationRecordId, applicationRecordData } =
|
||||
await this.laconicRegistry.createApplicationRecord({
|
||||
octokit,
|
||||
repository: data.project.repository,
|
||||
await this.registry.createApplicationRecord({
|
||||
appName: repo,
|
||||
packageJSON,
|
||||
appType: data.project!.template!,
|
||||
commitHash: data.commitHash!,
|
||||
repoUrl,
|
||||
});
|
||||
|
||||
// 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({
|
||||
project: data.project,
|
||||
branch: data.branch,
|
||||
@ -777,43 +594,60 @@ export class Service {
|
||||
createdBy: Object.assign(new User(), {
|
||||
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(
|
||||
projectId: string,
|
||||
deployer: Deployer
|
||||
): Promise<Deployer> {
|
||||
const deploymentProject = await this.db.getProjects({
|
||||
where: { id: projectId },
|
||||
relations: ['deployers']
|
||||
});
|
||||
const environmentVariablesObj = environmentVariables.reduce(
|
||||
(acc, env) => {
|
||||
acc[env.key] = env.value;
|
||||
|
||||
if (!deploymentProject[0].deployers) {
|
||||
deploymentProject[0].deployers = [];
|
||||
return acc;
|
||||
},
|
||||
{} 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(
|
||||
user: User,
|
||||
organizationSlug: string,
|
||||
data: AddProjectFromTemplateInput,
|
||||
lrn?: string,
|
||||
auctionParams?: AuctionParams,
|
||||
environmentVariables?: EnvironmentVariables[],
|
||||
): Promise<Project | undefined> {
|
||||
try {
|
||||
const octokit = await this.getOctokit(user.id);
|
||||
@ -844,9 +678,7 @@ export class Service {
|
||||
repository: gitRepo.data.full_name,
|
||||
// TODO: Set selected template
|
||||
template: 'webapp',
|
||||
paymentAddress: data.paymentAddress,
|
||||
txHash: data.txHash
|
||||
}, lrn, auctionParams, environmentVariables);
|
||||
});
|
||||
|
||||
if (!project || !project.id) {
|
||||
throw new Error('Failed to create project from template');
|
||||
@ -863,26 +695,18 @@ export class Service {
|
||||
user: User,
|
||||
organizationSlug: string,
|
||||
data: DeepPartial<Project>,
|
||||
lrn?: string,
|
||||
auctionParams?: AuctionParams,
|
||||
environmentVariables?: EnvironmentVariables[],
|
||||
): Promise<Project | undefined> {
|
||||
const organization = await this.db.getOrganization({
|
||||
where: {
|
||||
slug: organizationSlug,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
throw new Error('Organization does not exist');
|
||||
}
|
||||
|
||||
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 [owner, repo] = project.repository.split('/');
|
||||
|
||||
@ -895,59 +719,20 @@ export class Service {
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
if (auctionParams) {
|
||||
// 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 { 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);
|
||||
}
|
||||
// Create deployment with prod branch and latest commit
|
||||
const deployment = await this.createDeployment(user.id, octokit, {
|
||||
project,
|
||||
branch: project.prodBranch,
|
||||
environment: Environment.Production,
|
||||
domain: null,
|
||||
commitHash: latestCommit.sha,
|
||||
commitMessage: latestCommit.commit.message,
|
||||
});
|
||||
|
||||
await this.createRepoHook(octokit, project);
|
||||
|
||||
console.log('projectid is', project.id);
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
@ -997,9 +782,6 @@ export class Service {
|
||||
);
|
||||
const projects = await this.db.getProjects({
|
||||
where: { repository: repository.full_name },
|
||||
relations: {
|
||||
deployers: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (!projects.length) {
|
||||
@ -1016,29 +798,18 @@ export class Service {
|
||||
branch,
|
||||
});
|
||||
|
||||
const deployers = project.deployers;
|
||||
if (!deployers) {
|
||||
log(`No deployer present for project ${project.id}`)
|
||||
return;
|
||||
}
|
||||
|
||||
for (const deployer of deployers) {
|
||||
// Create deployment with branch and latest commit in GitHub data
|
||||
await this.createDeployment(project.ownerId, octokit,
|
||||
{
|
||||
project,
|
||||
branch,
|
||||
environment:
|
||||
project.prodBranch === branch
|
||||
? Environment.Production
|
||||
: Environment.Preview,
|
||||
domain,
|
||||
commitHash: headCommit.id,
|
||||
commitMessage: headCommit.message,
|
||||
deployer: deployer
|
||||
},
|
||||
);
|
||||
}
|
||||
// Create deployment with branch and latest commit in GitHub data
|
||||
await this.createDeployment(project.ownerId, octokit, {
|
||||
project,
|
||||
branch,
|
||||
environment:
|
||||
project.prodBranch === branch
|
||||
? Environment.Production
|
||||
: Environment.Preview,
|
||||
domain,
|
||||
commitHash: headCommit.id,
|
||||
commitMessage: headCommit.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1075,7 +846,6 @@ export class Service {
|
||||
relations: {
|
||||
project: true,
|
||||
domain: true,
|
||||
deployer: true,
|
||||
createdBy: true,
|
||||
},
|
||||
where: {
|
||||
@ -1089,24 +859,15 @@ export class Service {
|
||||
|
||||
const octokit = await this.getOctokit(user.id);
|
||||
|
||||
let newDeployment: Deployment;
|
||||
|
||||
if (oldDeployment.project.auctionId) {
|
||||
newDeployment = await this.createDeploymentFromAuction(oldDeployment.project, oldDeployment.deployer);
|
||||
} else {
|
||||
newDeployment = await this.createDeployment(user.id, octokit,
|
||||
{
|
||||
project: oldDeployment.project,
|
||||
// TODO: Put isCurrent field in project
|
||||
branch: oldDeployment.branch,
|
||||
environment: Environment.Production,
|
||||
domain: oldDeployment.domain,
|
||||
commitHash: oldDeployment.commitHash,
|
||||
commitMessage: oldDeployment.commitMessage,
|
||||
deployer: oldDeployment.deployer
|
||||
}
|
||||
);
|
||||
}
|
||||
const newDeployment = await this.createDeployment(user.id, octokit, {
|
||||
project: oldDeployment.project,
|
||||
// TODO: Put isCurrent field in project
|
||||
branch: oldDeployment.branch,
|
||||
environment: Environment.Production,
|
||||
domain: oldDeployment.domain,
|
||||
commitHash: oldDeployment.commitHash,
|
||||
commitMessage: oldDeployment.commitMessage,
|
||||
});
|
||||
|
||||
return newDeployment;
|
||||
}
|
||||
@ -1152,18 +913,16 @@ export class Service {
|
||||
},
|
||||
relations: {
|
||||
project: true,
|
||||
deployer: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (deployment && deployment.applicationDeploymentRecordId) {
|
||||
// If deployment is current, remove deployment for project subdomain as well
|
||||
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 =
|
||||
await this.laconicRegistry.getDeploymentRecordsByFilter({
|
||||
await this.registry.getDeploymentRecordsByFilter({
|
||||
application: deployment.applicationRecordId,
|
||||
url: currentDeploymentURL,
|
||||
});
|
||||
@ -1176,24 +935,14 @@ export class Service {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Multiple records are fetched, take the latest record
|
||||
const latestRecord = deploymentRecords
|
||||
.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
|
||||
await this.registry.createApplicationDeploymentRemovalRequest({
|
||||
deploymentId: deploymentRecords[0].id,
|
||||
});
|
||||
}
|
||||
|
||||
const result =
|
||||
await this.laconicRegistry.createApplicationDeploymentRemovalRequest({
|
||||
await this.registry.createApplicationDeploymentRemovalRequest({
|
||||
deploymentId: deployment.applicationDeploymentRecordId,
|
||||
deployerLrn: deployment.deployer.deployerLrn,
|
||||
auctionId: deployment.project.auctionId,
|
||||
payment: deployment.project.txHash
|
||||
});
|
||||
|
||||
await this.db.updateDeploymentById(deployment.id, {
|
||||
@ -1328,157 +1077,4 @@ export class Service {
|
||||
): Promise<boolean> {
|
||||
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 {
|
||||
application: string;
|
||||
auction: string;
|
||||
deployer: string;
|
||||
dns: string;
|
||||
meta: string;
|
||||
name: string;
|
||||
@ -70,35 +68,4 @@ export interface AddProjectFromTemplateInput {
|
||||
owner: string;
|
||||
name: string;
|
||||
isPrivate: boolean;
|
||||
paymentAddress: string;
|
||||
txHash: string;
|
||||
}
|
||||
|
||||
export interface AuctionParams {
|
||||
maxPrice: string,
|
||||
numProviders: number,
|
||||
}
|
||||
|
||||
export interface EnvironmentVariables {
|
||||
environments: string[],
|
||||
key: string,
|
||||
value: string,
|
||||
}
|
||||
|
||||
export interface DeployerRecord {
|
||||
id: string;
|
||||
names: string[];
|
||||
owners: string[];
|
||||
bondId: string;
|
||||
createTime: string;
|
||||
expiryTime: string;
|
||||
attributes: {
|
||||
apiUrl: string;
|
||||
minimumPayment: string | null;
|
||||
name: string;
|
||||
paymentAddress: string;
|
||||
publicKey: string;
|
||||
type: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
@ -1,14 +1,10 @@
|
||||
import assert from 'assert';
|
||||
import debug from 'debug';
|
||||
import fs from 'fs-extra';
|
||||
import { Octokit } from 'octokit';
|
||||
import path from 'path';
|
||||
import toml from 'toml';
|
||||
import debug from 'debug';
|
||||
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { Config } from './config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
||||
import { PackageJSON } from './types';
|
||||
|
||||
const log = debug('snowball:utils');
|
||||
|
||||
@ -80,64 +76,3 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
|
||||
export const sleep = async (ms: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export const getRepoDetails = async (
|
||||
octokit: Octokit,
|
||||
repository: string,
|
||||
commitHash: string | undefined,
|
||||
): Promise<{
|
||||
repo: string;
|
||||
packageJSON: PackageJSON;
|
||||
repoUrl: string;
|
||||
}> => {
|
||||
const [owner, repo] = repository.split('/');
|
||||
const { data: packageJSONData } = await octokit.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'package.json',
|
||||
ref: commitHash,
|
||||
});
|
||||
|
||||
if (!packageJSONData) {
|
||||
throw new Error('Package.json file not found');
|
||||
}
|
||||
|
||||
assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file');
|
||||
const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content));
|
||||
|
||||
assert(packageJSON.name, "name field doesn't exist in package.json");
|
||||
|
||||
const repoUrl = (
|
||||
await octokit.rest.repos.get({
|
||||
owner,
|
||||
repo,
|
||||
})
|
||||
).data.html_url;
|
||||
|
||||
return {
|
||||
repo,
|
||||
packageJSON,
|
||||
repoUrl
|
||||
};
|
||||
}
|
||||
|
||||
// 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 { getConfig } from '../src/utils';
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
|
||||
const log = debug('snowball:delete-database');
|
||||
|
||||
@ -11,7 +13,7 @@ const deleteFile = async (filePath: string) => {
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const config = await getConfig();
|
||||
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
|
||||
deleteFile(config.database.dbPath);
|
||||
};
|
||||
|
@ -1,8 +1,8 @@
|
||||
[
|
||||
{
|
||||
"id": "2379cf1f-a232-4ad2-ae14-4d881131cc26",
|
||||
"name": "Deploy Tools",
|
||||
"slug": "deploy-tools"
|
||||
"name": "Snowball Tools",
|
||||
"slug": "snowball-tools-1"
|
||||
},
|
||||
{
|
||||
"id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb",
|
||||
|
@ -16,6 +16,8 @@ import {
|
||||
getEntities,
|
||||
loadAndSaveData
|
||||
} from '../src/utils';
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
|
||||
const log = debug('snowball:initialize-database');
|
||||
|
||||
@ -154,7 +156,7 @@ const generateTestData = async (dataSource: DataSource) => {
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const config = await getConfig();
|
||||
const config = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
const isDbPresent = await checkFileExists(config.database.dbPath);
|
||||
|
||||
if (!isDbPresent) {
|
||||
|
@ -1,40 +1,39 @@
|
||||
import debug from 'debug';
|
||||
|
||||
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||
import { Registry } from '@snowballtools/laconic-sdk';
|
||||
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
import { Config } from '../src/config';
|
||||
import { getConfig } from '../src/utils';
|
||||
|
||||
const log = debug('snowball:initialize-registry');
|
||||
|
||||
const DENOM = 'alnt';
|
||||
const DENOM = 'aphoton';
|
||||
const BOND_AMOUNT = '1000000000';
|
||||
|
||||
async function main () {
|
||||
const { registryConfig } = await getConfig();
|
||||
const { registryConfig } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
|
||||
// TODO: Get authority names from args
|
||||
const authorityNames = ['snowballtools', registryConfig.authority];
|
||||
|
||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, {chainId: registryConfig.chainId});
|
||||
const registry = new Registry(registryConfig.gqlEndpoint, registryConfig.restEndpoint, registryConfig.chainId);
|
||||
|
||||
const bondId = await registry.getNextBondId(registryConfig.privateKey);
|
||||
log('bondId:', bondId);
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||
|
||||
await registry.createBond(
|
||||
{ denom: DENOM, amount: BOND_AMOUNT },
|
||||
registryConfig.privateKey,
|
||||
fee
|
||||
registryConfig.fee
|
||||
);
|
||||
|
||||
for await (const name of authorityNames) {
|
||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, fee);
|
||||
await registry.reserveAuthority({ name }, registryConfig.privateKey, registryConfig.fee);
|
||||
log('Reserved authority name:', name);
|
||||
await registry.setAuthorityBond(
|
||||
{ name, bondId },
|
||||
registryConfig.privateKey,
|
||||
fee
|
||||
registryConfig.fee
|
||||
);
|
||||
log(`Bond ${bondId} set for authority ${name}`);
|
||||
}
|
||||
|
@ -2,20 +2,22 @@ import debug from 'debug';
|
||||
import { DataSource } from 'typeorm';
|
||||
import path from 'path';
|
||||
|
||||
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||
import { Registry } from '@snowballtools/laconic-sdk';
|
||||
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
import { getConfig } from '../src/utils';
|
||||
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
|
||||
|
||||
const log = debug('snowball:publish-deploy-records');
|
||||
|
||||
async function main() {
|
||||
const { registryConfig, database, misc } = await getConfig();
|
||||
async function main () {
|
||||
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId }
|
||||
registryConfig.chainId
|
||||
);
|
||||
|
||||
const dataSource = new DataSource({
|
||||
@ -38,7 +40,7 @@ async function main() {
|
||||
});
|
||||
|
||||
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 = {
|
||||
type: 'ApplicationDeploymentRecord',
|
||||
@ -59,8 +61,6 @@ async function main() {
|
||||
url
|
||||
};
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||
|
||||
const result = await registry.setRecord(
|
||||
{
|
||||
privateKey: registryConfig.privateKey,
|
||||
@ -68,12 +68,12 @@ async function main() {
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
fee
|
||||
registryConfig.fee
|
||||
);
|
||||
|
||||
// Remove deployment for project subdomain if deployment is for production environment
|
||||
if (deployment.environment === Environment.Production) {
|
||||
applicationDeploymentRecord.url = `https://${deployment.project.name}.${deployment.deployer.baseDomain}`;
|
||||
applicationDeploymentRecord.url = `https://${deployment.project.subDomain}`
|
||||
|
||||
await registry.setRecord(
|
||||
{
|
||||
@ -82,12 +82,12 @@ async function main() {
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
fee
|
||||
registryConfig.fee
|
||||
);
|
||||
}
|
||||
|
||||
log('Application deployment record data:', applicationDeploymentRecord);
|
||||
log(`Application deployment record published: ${result.id}`);
|
||||
log(`Application deployment record published: ${result.data.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,20 +2,22 @@ import debug from 'debug';
|
||||
import { DataSource } from 'typeorm';
|
||||
import path from 'path';
|
||||
|
||||
import { parseGasAndFees, Registry } from '@cerc-io/registry-sdk';
|
||||
import { Registry } from '@cerc-io/laconic-sdk';
|
||||
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
import { getConfig } from '../src/utils';
|
||||
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
||||
|
||||
const log = debug('snowball:publish-deployment-removal-records');
|
||||
|
||||
async function main () {
|
||||
const { registryConfig, database } = await getConfig();
|
||||
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
|
||||
const registry = new Registry(
|
||||
registryConfig.gqlEndpoint,
|
||||
registryConfig.restEndpoint,
|
||||
{ chainId: registryConfig.chainId }
|
||||
registryConfig.chainId
|
||||
);
|
||||
|
||||
const dataSource = new DataSource({
|
||||
@ -45,8 +47,6 @@ async function main () {
|
||||
request: deployment.applicationDeploymentRemovalRequestId,
|
||||
}
|
||||
|
||||
const fee = parseGasAndFees(registryConfig.fee.gas, registryConfig.fee.fees);
|
||||
|
||||
const result = await registry.setRecord(
|
||||
{
|
||||
privateKey: registryConfig.privateKey,
|
||||
@ -54,11 +54,11 @@ async function main () {
|
||||
bondId: registryConfig.bondId
|
||||
},
|
||||
'',
|
||||
fee
|
||||
registryConfig.fee
|
||||
);
|
||||
|
||||
log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
|
||||
log(`Application deployment removal record published: ${result.id}`);
|
||||
log(`Application deployment removal record published: ${result.data.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
REGISTRY_BOND_ID=
|
||||
DEPLOYER_LRN=
|
||||
AUTHORITY=
|
@ -17,7 +17,7 @@
|
||||
```bash
|
||||
./deploy-frontend.staging.sh
|
||||
```
|
||||
|
||||
|
||||
- To deploy frontend app to `dashboard.apps.snowballtools.com`
|
||||
|
||||
```bash
|
||||
@ -51,14 +51,14 @@
|
||||
|
||||
```bash
|
||||
# Account balance
|
||||
yarn laconic registry account get
|
||||
yarn laconic cns account get
|
||||
|
||||
# Bond balance
|
||||
yarn laconic registry bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
yarn laconic cns bond get --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
```
|
||||
|
||||
- Command to refill bond
|
||||
|
||||
```bash
|
||||
yarn laconic registry bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type alnt --quantity 10000000
|
||||
yarn laconic cns bond refill --id 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32 --type aphoton --quantity 10000000
|
||||
```
|
||||
|
@ -1,10 +1,9 @@
|
||||
services:
|
||||
registry:
|
||||
cns:
|
||||
restEndpoint: 'http://console.laconic.com:1317'
|
||||
gqlEndpoint: 'http://console.laconic.com:9473/api'
|
||||
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
|
||||
bondId: 89c75c7bc5759861d10285aff6f9e7227d6855e446b77ad5d8324822dfec7deb
|
||||
chainId: laconic_9000-1
|
||||
gas:
|
||||
fees:
|
||||
gasPrice: 1
|
||||
gas: 1200000
|
||||
fees: 200000aphoton
|
||||
|
@ -1,8 +1,9 @@
|
||||
services:
|
||||
registry:
|
||||
rpcEndpoint: https://laconicd-sapo.laconic.com
|
||||
gqlEndpoint: https://laconicd-sapo.laconic.com/api
|
||||
userKey:
|
||||
bondId:
|
||||
chainId: laconic_9000-2
|
||||
gasPrice: 1alnt
|
||||
cns:
|
||||
restEndpoint: http://console.laconic.com:1317
|
||||
gqlEndpoint: http://console.laconic.com:9473/api
|
||||
userKey: 489c9dd3931c2a2d4dd77973302dc5eb01e2a49552f9d932c58d9da823512311
|
||||
bondId: 99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32
|
||||
chainId: laconic_9000-1
|
||||
gas: 1200000
|
||||
fees: 200000aphoton
|
||||
|
@ -1,10 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
source .env
|
||||
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
|
||||
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
|
||||
echo "Using AUTHORITY: $AUTHORITY"
|
||||
|
||||
# Repository URL
|
||||
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)
|
||||
|
||||
CONFIG_FILE=config.yml
|
||||
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "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
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
cat >./records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
application: crn://snowballtools/applications/snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
dns: dashboard
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_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
|
||||
cat >./records/application-record.yml <<EOF
|
||||
record:
|
||||
@ -37,16 +55,16 @@ record:
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: deploy-frontend
|
||||
name: snowballtools-base-frontend
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
echo "Files generated successfully"
|
||||
echo "Files generated successfully."
|
||||
|
||||
RECORD_FILE=records/application-record.yml
|
||||
|
||||
# Publish ApplicationRecord
|
||||
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to publish record"
|
||||
@ -57,17 +75,17 @@ echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/deploy-frontend"
|
||||
REGISTRY_APP_CRN="crn://snowballtools/applications/snowballtools-base-frontend"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
rc=$?
|
||||
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
|
||||
fi
|
||||
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=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set hash"
|
||||
@ -75,16 +93,16 @@ if [ $rc -ne 0 ]; then
|
||||
fi
|
||||
sleep 2
|
||||
# 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=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set release"
|
||||
exit $rc
|
||||
fi
|
||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||
|
||||
# Check if record found for REGISTRY_APP_LRN
|
||||
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
||||
# Check if record found for REGISTRY_APP_CRN
|
||||
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN")
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query name"
|
||||
@ -92,53 +110,14 @@ if [ $rc -ne 0 ]; then
|
||||
fi
|
||||
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_LRN."
|
||||
echo "No record found for $REGISTRY_APP_CRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
echo "FATAL: Failed to query deployment request"
|
||||
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
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "staging-snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "staging-snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
@ -31,7 +31,7 @@ record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
application: lrn://staging-snowballtools/applications/staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
dns: dashboard.staging.apps.snowballtools.com
|
||||
config:
|
||||
env:
|
||||
@ -41,7 +41,6 @@ record:
|
||||
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
|
||||
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_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
||||
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
||||
@ -71,7 +70,7 @@ echo "Files generated successfully."
|
||||
RECORD_FILE=staging-records/application-record.yml
|
||||
|
||||
# Publish ApplicationRecord
|
||||
publish_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
publish_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to publish record"
|
||||
@ -82,17 +81,17 @@ echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_LRN="lrn://staging-snowballtools/applications/staging-snowballtools-base-frontend"
|
||||
REGISTRY_APP_CRN="crn://staging-snowballtools/applications/staging-snowballtools-base-frontend"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
rc=$?
|
||||
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
|
||||
fi
|
||||
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=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set hash"
|
||||
@ -100,16 +99,16 @@ if [ $rc -ne 0 ]; then
|
||||
fi
|
||||
sleep 2
|
||||
# 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=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to set release"
|
||||
exit $rc
|
||||
fi
|
||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||
|
||||
# Check if record found for REGISTRY_APP_LRN
|
||||
query_response=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
|
||||
# Check if record found for REGISTRY_APP_CRN
|
||||
query_response=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN")
|
||||
rc=$?
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query name"
|
||||
@ -117,14 +116,14 @@ if [ $rc -ne 0 ]; then
|
||||
fi
|
||||
APP_RECORD=$(echo $query_response | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_LRN."
|
||||
echo "No record found for $REGISTRY_APP_CRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RECORD_FILE=staging-records/application-deployment-request.yml
|
||||
|
||||
sleep 2
|
||||
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
|
||||
deployment_response=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE)
|
||||
if [ $rc -ne 0 ]; then
|
||||
echo "FATAL: Failed to query deployment request"
|
||||
exit $rc
|
||||
|
@ -4,6 +4,6 @@
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@cerc-io/laconic-registry-cli": "^0.2.9"
|
||||
"@snowballtools/laconic-registry-cli": "^0.1.13"
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,17 @@
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: deploy-frontend@1.0.0
|
||||
application: lrn://vaasl/applications/deploy-frontend@1.0.0
|
||||
dns: deploy
|
||||
version: "1.0.0"
|
||||
name: snowballtools-base-frontend@0.1.8
|
||||
application: crn://snowballtools/applications/snowballtools-base-frontend@0.1.8
|
||||
dns: dashboard
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_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_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
||||
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
meta:
|
||||
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
|
||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
|
@ -4,5 +4,5 @@ record:
|
||||
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
|
||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||
app_type: webapp
|
||||
name: deploy-frontend
|
||||
app_version: 1.0.0
|
||||
name: snowballtools-base-frontend
|
||||
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_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
|
||||
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_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
|
||||
|
@ -22,7 +22,7 @@ REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be
|
||||
APP_NAME=deployment-test-app
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
@ -51,7 +51,7 @@ record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: $APP_NAME@$PACKAGE_VERSION
|
||||
application: lrn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
|
||||
application: crn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
|
||||
dns: deployment-ci-test
|
||||
config:
|
||||
env:
|
||||
@ -67,31 +67,31 @@ EOF
|
||||
echo "Record files generated successfully."
|
||||
|
||||
# Publish ApplicationRecord
|
||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_LRN="lrn://snowballtools/applications/$APP_NAME"
|
||||
REGISTRY_APP_CRN="crn://snowballtools/applications/$APP_NAME"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
sleep 2
|
||||
# Set name if latest release
|
||||
yarn --silent laconic -c $CONFIG_FILE registry name set "$REGISTRY_APP_LRN" "$RECORD_ID"
|
||||
echo "$REGISTRY_APP_LRN set for ApplicationRecord"
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||
|
||||
# Check if record exists for REGISTRY_APP_LRN
|
||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN" | jq '.[0]')
|
||||
# Check if record exists for REGISTRY_APP_CRN
|
||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_LRN."
|
||||
echo "No record found for $REGISTRY_APP_CRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
|
||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRequest published"
|
||||
echo $DEPLOYMENT_REQUEST_ID
|
||||
|
||||
@ -102,7 +102,7 @@ MAX_RETRIES=20
|
||||
# Check that a ApplicationDeploymentRecord is published
|
||||
retry_count=0
|
||||
while true; do
|
||||
deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
|
||||
deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
|
||||
len_deployment_records=$(echo $deployment_records_response | jq 'length')
|
||||
|
||||
# Check if number of records returned is 0
|
||||
@ -140,7 +140,7 @@ fetched_url=$(echo $deployment_records_response | jq -r '.[0].attributes.url')
|
||||
|
||||
retry_count=0
|
||||
max_retries=10
|
||||
retry_interval=10
|
||||
retry_interval=5
|
||||
while true; do
|
||||
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
||||
if [ "$url_response" = "200" ]; then
|
||||
@ -170,14 +170,14 @@ record:
|
||||
EOF
|
||||
|
||||
sleep 2
|
||||
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE registry record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
||||
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRemovalRequest published"
|
||||
echo $REMOVAL_REQUEST_ID
|
||||
|
||||
# Check that an ApplicationDeploymentRemovalRecord is published
|
||||
retry_count=0
|
||||
while true; do
|
||||
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
||||
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
||||
len_removal_records=$(echo $removal_records_response | jq 'length')
|
||||
|
||||
# Check if number of records returned is 0
|
||||
|
@ -15,5 +15,3 @@ VITE_BUGSNAG_API_KEY=
|
||||
VITE_PASSKEY_WALLET_RPID=
|
||||
VITE_TURNKEY_API_BASE_URL=
|
||||
VITE_TURNKEY_ORGANIZATION_ID=
|
||||
|
||||
VITE_LACONICD_CHAIN_ID=
|
||||
|
@ -44,7 +44,7 @@ yarn dev
|
||||
|
||||
## Deployment
|
||||
|
||||
From the root of the project,
|
||||
From the root of the project, run:
|
||||
|
||||
### Staging
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 3000",
|
||||
@ -16,11 +16,8 @@
|
||||
"@bugsnag/browser-performance": "^2.4.1",
|
||||
"@bugsnag/js": "^7.22.7",
|
||||
"@bugsnag/plugin-react": "^7.22.7",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||
"@fontsource/inter": "^5.0.16",
|
||||
"@mui/material": "^6.1.3",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
@ -30,6 +27,10 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@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/smartwallet-alchemy-light": "^0.2.0",
|
||||
"@snowballtools/types": "^0.2.0",
|
||||
@ -42,8 +43,8 @@
|
||||
"@turnkey/sdk-react": "^0.1.0",
|
||||
"@turnkey/webauthn-stamper": "^0.5.0",
|
||||
"@walletconnect/ethereum-provider": "^2.12.2",
|
||||
"@web3modal/siwe": "4.0.5",
|
||||
"@web3modal/wagmi": "4.0.5",
|
||||
"@web3modal/siwe": "^4.0.5",
|
||||
"@web3modal/wagmi": "^4.0.5",
|
||||
"assert": "^2.1.0",
|
||||
"axios": "^1.6.7",
|
||||
"clsx": "^2.1.0",
|
||||
@ -64,12 +65,11 @@
|
||||
"react-oauth-popup": "^1.0.5",
|
||||
"react-router-dom": "^6.20.1",
|
||||
"react-timer-hook": "^3.0.7",
|
||||
"siwe": "2.1.4",
|
||||
"siwe": "^2.1.4",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"usehooks-ts": "^2.15.1",
|
||||
"uuid": "^9.0.1",
|
||||
"viem": "^2.7.11",
|
||||
"wagmi": "2.5.7",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1 +0,0 @@
|
||||
350e9ac2-8b27-4a79-9a82-78cfdb68ef71=0eacb7ae462f82c8b0199d28193b0bfa5265973dbb1fe991eec2cab737dfc1ec
|
@ -12,7 +12,7 @@ import Index from './pages';
|
||||
import AuthPage from './pages/AuthPage';
|
||||
import { DashboardLayout } from './pages/org-slug/layout';
|
||||
import Web3Provider from 'context/Web3Provider';
|
||||
import { BASE_URL } from 'utils/constants';
|
||||
import { baseUrl } from 'utils/constants';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -50,26 +50,25 @@ const router = createBrowserRouter([
|
||||
path: '/login',
|
||||
element: <AuthPage />,
|
||||
},
|
||||
{
|
||||
path: '/signup',
|
||||
element: <AuthPage />,
|
||||
},
|
||||
]);
|
||||
|
||||
function App() {
|
||||
// Hacky way of checking session
|
||||
// TODO: Handle redirect backs
|
||||
useEffect(() => {
|
||||
fetch(`${BASE_URL}/auth/session`, {
|
||||
fetch(`${baseUrl}/auth/session`, {
|
||||
credentials: 'include',
|
||||
}).then((res) => {
|
||||
const path = window.location.pathname;
|
||||
if (res.status !== 200) {
|
||||
localStorage.clear();
|
||||
|
||||
if (path !== '/login') {
|
||||
const path = window.location.pathname;
|
||||
if (path !== '/login' && path !== '/signup') {
|
||||
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 FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
|
||||
@ -13,19 +12,14 @@ const setStopWatchOffset = (time: string) => {
|
||||
|
||||
interface StopwatchProps extends Omit<FormatMilliSecondProps, 'time'> {
|
||||
offsetTimestamp: Date;
|
||||
isPaused: boolean;
|
||||
}
|
||||
|
||||
const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => {
|
||||
const { totalSeconds, pause, start } = useStopwatch({
|
||||
const Stopwatch = ({ offsetTimestamp, ...props }: StopwatchProps) => {
|
||||
const { totalSeconds } = useStopwatch({
|
||||
autoStart: true,
|
||||
offsetTimestamp: offsetTimestamp,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
isPaused ? pause() : start();
|
||||
}, [isPaused]);
|
||||
|
||||
return <FormatMillisecond time={totalSeconds * 1000} {...props} />;
|
||||
};
|
||||
|
||||
|
@ -10,16 +10,11 @@ import {
|
||||
LinkChainIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { TagProps } from 'components/shared/Tag';
|
||||
import {
|
||||
ArrowRightCircleFilledIcon,
|
||||
LoadingIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
|
||||
interface ChangeStateToProductionDialogProps extends ConfirmDialogProps {
|
||||
deployment: Deployment;
|
||||
newDeployment?: Deployment;
|
||||
domains: Domain[];
|
||||
isConfirmButtonLoading?: boolean;
|
||||
}
|
||||
|
||||
export const ChangeStateToProductionDialog = ({
|
||||
@ -29,7 +24,6 @@ export const ChangeStateToProductionDialog = ({
|
||||
open,
|
||||
handleCancel,
|
||||
handleConfirm,
|
||||
isConfirmButtonLoading,
|
||||
...props
|
||||
}: ChangeStateToProductionDialogProps) => {
|
||||
const currentChip = {
|
||||
@ -47,14 +41,6 @@ export const ChangeStateToProductionDialog = ({
|
||||
handleCancel={handleCancel}
|
||||
open={open}
|
||||
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-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,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 axios from 'axios';
|
||||
import { Deployment } from 'gql-client';
|
||||
|
||||
import { DeployStep, DeployStatus } from './DeployStep';
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
@ -9,37 +7,13 @@ import { Heading } from '../../shared/Heading';
|
||||
import { Button } from '../../shared/Button';
|
||||
import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon';
|
||||
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 client = useGQLClient();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const projectId = searchParams.get('projectId');
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [deployment, setDeployment] = useState<Deployment>();
|
||||
const [record, setRecord] = useState<Record>();
|
||||
|
||||
const handleOpen = () => setOpen(!open);
|
||||
|
||||
const navigate = useNavigate();
|
||||
@ -49,67 +23,13 @@ const Deploy = () => {
|
||||
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(() => {
|
||||
fetchDeployment();
|
||||
fetchDeploymentRecords();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchDeploymentRecords();
|
||||
}, FETCH_DEPLOYMENTS_INTERVAL);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [fetchDeployment, fetchDeploymentRecords]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (record.lastState === 'DEPLOYED') {
|
||||
const timerID = setTimeout(() => {
|
||||
navigate(`/${orgSlug}/projects/create/success/${projectId}`);
|
||||
}
|
||||
}, [record]);
|
||||
}, TIMEOUT_DURATION);
|
||||
|
||||
return () => clearInterval(timerID);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-7">
|
||||
@ -122,7 +42,6 @@ const Deploy = () => {
|
||||
<ClockOutlineIcon size={16} className="text-elements-mid-em" />
|
||||
<Stopwatch
|
||||
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
||||
isPaused={isDeploymentFailed}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -141,36 +60,30 @@ const Deploy = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isDeploymentFailed ? (
|
||||
<div>
|
||||
<DeployStep
|
||||
title={record ? 'Submitted' : 'Submitting'}
|
||||
status={record ? DeployStatus.COMPLETE : DeployStatus.PROCESSING}
|
||||
step="1"
|
||||
/>
|
||||
|
||||
<DeployStep
|
||||
title={
|
||||
record && record.lastState === 'DEPLOYED'
|
||||
? 'Deployed'
|
||||
: 'Deploying'
|
||||
}
|
||||
status={
|
||||
!record
|
||||
? DeployStatus.NOT_STARTED
|
||||
: record.lastState === 'DEPLOYED'
|
||||
? DeployStatus.COMPLETE
|
||||
: DeployStatus.PROCESSING
|
||||
}
|
||||
step="2"
|
||||
startTime={Date.now().toString()}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<DeployStep title={record!.lastState} status={DeployStatus.ERROR} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<DeployStep
|
||||
title="Building"
|
||||
status={DeployStatus.COMPLETE}
|
||||
step="1"
|
||||
processTime="72000"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Deployment summary"
|
||||
status={DeployStatus.PROCESSING}
|
||||
step="2"
|
||||
startTime={Date.now().toString()}
|
||||
/>
|
||||
<DeployStep
|
||||
title="Running checks"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="3"
|
||||
/>
|
||||
<DeployStep
|
||||
title="Assigning domains"
|
||||
status={DeployStatus.NOT_STARTED}
|
||||
step="4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,16 +1,27 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import FormatMillisecond from '../../FormatMilliSecond';
|
||||
import processLogs from '../../../assets/process-logs.json';
|
||||
import { cn } from 'utils/classnames';
|
||||
import {
|
||||
CheckRoundFilledIcon,
|
||||
ClockOutlineIcon,
|
||||
CopyIcon,
|
||||
LoaderIcon,
|
||||
MinusCircleIcon,
|
||||
PlusIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { useIntersectionObserver } from 'usehooks-ts';
|
||||
|
||||
enum DeployStatus {
|
||||
PROCESSING = 'progress',
|
||||
COMPLETE = 'complete',
|
||||
NOT_STARTED = 'notStarted',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
interface DeployStepsProps {
|
||||
@ -21,11 +32,35 @@ interface DeployStepsProps {
|
||||
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 (
|
||||
<div className="border-b border-border-separator">
|
||||
{/* Collapisble trigger */}
|
||||
<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')}>
|
||||
{/* Icon */}
|
||||
@ -38,6 +73,12 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<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>
|
||||
|
||||
{/* Title */}
|
||||
@ -55,10 +96,7 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
|
||||
{status === DeployStatus.PROCESSING && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ClockOutlineIcon size={16} className="text-elements-low-em" />
|
||||
<Stopwatch
|
||||
offsetTimestamp={setStopWatchOffset(startTime!)}
|
||||
isPaused={false}
|
||||
/>
|
||||
<Stopwatch offsetTimestamp={setStopWatchOffset(startTime!)} />
|
||||
</div>
|
||||
)}
|
||||
{status === DeployStatus.COMPLETE && (
|
||||
@ -69,9 +107,51 @@ const DeployStep = ({ step, status, title, startTime }: DeployStepsProps) => {
|
||||
size={15}
|
||||
/>
|
||||
</div>
|
||||
<FormatMillisecond time={Number(processTime)} />{' '}
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -38,9 +38,36 @@ export const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
navigate(
|
||||
`configure?owner=${repository.owner?.login}&name=${repository.name}&defaultBranch=${repository.default_branch}&fullName=${repository.full_name}&orgSlug=${orgSlug}`,
|
||||
);
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const { addProject } = await client.addProject(orgSlug, {
|
||||
name: `${repository.owner?.login}-${repository.name}`,
|
||||
prodBranch: repository.default_branch as string,
|
||||
repository: repository.full_name,
|
||||
// TODO: Compute template from repo
|
||||
template: 'webapp',
|
||||
});
|
||||
if (addProject) {
|
||||
navigate(`import?projectId=${addProject.id}`);
|
||||
} else {
|
||||
toast({
|
||||
id: 'failed-to-create-project',
|
||||
title: 'Failed to create project',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error((error as Error).message);
|
||||
toast({
|
||||
id: 'failed-to-create-project',
|
||||
title: 'Failed to create project',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [client, repository, orgSlug, setIsLoading, navigate, toast]);
|
||||
|
||||
return (
|
||||
|
@ -64,9 +64,9 @@ export const RepositoryList = () => {
|
||||
|
||||
// Check if selected account is an organization
|
||||
if (selectedAccount.value === gitUser.login) {
|
||||
query = query + ` user:${selectedAccount.value}`;
|
||||
query = query + ` user:${selectedAccount}`;
|
||||
} else {
|
||||
query = query + ` org:${selectedAccount.value}`;
|
||||
query = query + ` org:${selectedAccount}`;
|
||||
}
|
||||
|
||||
const result = await octokit.rest.search.repos({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
Deployment,
|
||||
DeploymentStatus,
|
||||
@ -6,15 +6,6 @@ import {
|
||||
Environment,
|
||||
Project,
|
||||
} from 'gql-client';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import {
|
||||
BranchStrokeIcon,
|
||||
@ -27,23 +18,12 @@ import {
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { OverflownText } from 'components/shared/OverflownText';
|
||||
import { Tag, TagTheme } from 'components/shared/Tag';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { SHORT_COMMIT_HASH_LENGTH } from '../../../../constants';
|
||||
import { formatAddress } from '../../../../utils/format';
|
||||
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 {
|
||||
deployment: Deployment;
|
||||
currentDeployment: Deployment;
|
||||
@ -68,14 +48,6 @@ const DeploymentDetailsCard = ({
|
||||
project,
|
||||
prodBranchDomains,
|
||||
}: 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) => {
|
||||
if (
|
||||
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(
|
||||
(className?: string) => {
|
||||
return (
|
||||
<Tooltip title="Click to view build logs">
|
||||
<div className={className} style={{ cursor: 'pointer' }}>
|
||||
<Tag
|
||||
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||
size="xs"
|
||||
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||
onClick={fetchDeploymentLogs}
|
||||
>
|
||||
{deployment.status}
|
||||
</Tag>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className={className}>
|
||||
<Tag
|
||||
leftIcon={getIconByDeploymentStatus(deployment.status)}
|
||||
size="xs"
|
||||
type={STATUS_COLORS[deployment.status] ?? 'neutral'}
|
||||
>
|
||||
{deployment.status}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[deployment.status, deployment.commitHash],
|
||||
@ -132,7 +83,7 @@ const DeploymentDetailsCard = ({
|
||||
|
||||
return (
|
||||
<div className="flex md:flex-row flex-col gap-6 py-4 px-3 pb-6 mb-2 last:mb-0 last:pb-4 border-b border-border-separator last:border-b-transparent relative">
|
||||
<div className="flex-1 flex justify-between w-full md:max-w-[30%] lg:max-w-[33%]">
|
||||
<div className="flex-1 flex justify-between w-full md:max-w-[25%] lg:max-w-[28%]">
|
||||
<div className="flex-1 w-full space-y-2 max-w-[90%] sm:max-w-full">
|
||||
{/* DEPLOYMENT URL */}
|
||||
{deployment.url && (
|
||||
@ -145,12 +96,7 @@ const DeploymentDetailsCard = ({
|
||||
</OverflownText>
|
||||
</Heading>
|
||||
)}
|
||||
{deployment.deployer.deployerLrn && (
|
||||
<span className="text-sm text-elements-low-em tracking-tight block mt-2">
|
||||
Deployer LRN: {deployment.deployer.deployerLrn}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm text-elements-low-em tracking-tight block">
|
||||
<span className="text-sm text-elements-low-em tracking-tight">
|
||||
{deployment.environment === Environment.Production
|
||||
? `Production ${deployment.isCurrent ? '(Current)' : ''}`
|
||||
: 'Preview'}
|
||||
@ -216,20 +162,6 @@ const DeploymentDetailsCard = ({
|
||||
prodBranchDomains={prodBranchDomains}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -23,7 +23,6 @@ import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { cn } from 'utils/classnames';
|
||||
import { ChangeStateToProductionDialog } from 'components/projects/Dialog/ChangeStateToProductionDialog';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { DeleteDeploymentDialog } from 'components/projects/Dialog/DeleteDeploymentDialog';
|
||||
|
||||
interface DeploymentMenuProps extends ComponentPropsWithRef<'div'> {
|
||||
deployment: Deployment;
|
||||
@ -47,16 +46,12 @@ export const DeploymentMenu = ({
|
||||
|
||||
const [changeToProduction, setChangeToProduction] = useState(false);
|
||||
const [redeployToProduction, setRedeployToProduction] = useState(false);
|
||||
const [deleteDeploymentDialog, setDeleteDeploymentDialog] = useState(false);
|
||||
const [isConfirmDeleteLoading, setIsConfirmDeleteLoading] = useState(false);
|
||||
const [rollbackDeployment, setRollbackDeployment] = useState(false);
|
||||
const [assignDomainDialog, setAssignDomainDialog] = useState(false);
|
||||
const [isConfirmButtonLoading, setConfirmButtonLoadingLoading] =
|
||||
useState(false);
|
||||
|
||||
const updateDeployment = async () => {
|
||||
const isUpdated = await client.updateDeploymentToProd(deployment.id);
|
||||
if (isUpdated.updateDeploymentToProd) {
|
||||
if (isUpdated) {
|
||||
await onUpdate();
|
||||
toast({
|
||||
id: 'deployment_changed_to_production',
|
||||
@ -76,8 +71,7 @@ export const DeploymentMenu = ({
|
||||
|
||||
const redeployToProd = async () => {
|
||||
const isRedeployed = await client.redeployToProd(deployment.id);
|
||||
setConfirmButtonLoadingLoading(false);
|
||||
if (isRedeployed.redeployToProd) {
|
||||
if (isRedeployed) {
|
||||
await onUpdate();
|
||||
toast({
|
||||
id: 'redeployed_to_production',
|
||||
@ -100,7 +94,7 @@ export const DeploymentMenu = ({
|
||||
project.id,
|
||||
deployment.id,
|
||||
);
|
||||
if (isRollbacked.rollbackDeployment) {
|
||||
if (isRollbacked) {
|
||||
await onUpdate();
|
||||
toast({
|
||||
id: 'deployment_rolled_back',
|
||||
@ -120,15 +114,11 @@ export const DeploymentMenu = ({
|
||||
|
||||
const deleteDeployment = async () => {
|
||||
const isDeleted = await client.deleteDeployment(deployment.id);
|
||||
|
||||
setIsConfirmDeleteLoading(false);
|
||||
setDeleteDeploymentDialog((preVal) => !preVal);
|
||||
|
||||
if (isDeleted.deleteDeployment) {
|
||||
if (isDeleted) {
|
||||
await onUpdate();
|
||||
toast({
|
||||
id: 'deployment_removal_requested',
|
||||
title: 'Deployment removal requested',
|
||||
id: 'deployment_deleted',
|
||||
title: 'Deployment deleted',
|
||||
variant: 'success',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
@ -212,7 +202,7 @@ export const DeploymentMenu = ({
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||
onClick={() => setDeleteDeploymentDialog((preVal) => !preVal)}
|
||||
onClick={() => deleteDeployment()}
|
||||
>
|
||||
<CrossCircleIcon /> Delete deployment
|
||||
</MenuItem>
|
||||
@ -238,13 +228,11 @@ export const DeploymentMenu = ({
|
||||
open={redeployToProduction}
|
||||
confirmButtonTitle="Redeploy"
|
||||
handleConfirm={async () => {
|
||||
setConfirmButtonLoadingLoading(true);
|
||||
await redeployToProd();
|
||||
setRedeployToProduction((preVal) => !preVal);
|
||||
}}
|
||||
deployment={deployment}
|
||||
domains={deployment.domain ? [deployment.domain] : []}
|
||||
isConfirmButtonLoading={isConfirmButtonLoading}
|
||||
/>
|
||||
{Boolean(currentDeployment) && (
|
||||
<ChangeStateToProductionDialog
|
||||
@ -265,15 +253,6 @@ export const DeploymentMenu = ({
|
||||
open={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
|
||||
label={
|
||||
"Deleting your project is irreversible. Enter your project's name " +
|
||||
'"' +
|
||||
project.name +
|
||||
'"' +
|
||||
' below to confirm you want to permanently delete it:'
|
||||
}
|
||||
id="input"
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { User } from 'gql-client';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { NavLink, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Organization, User } from 'gql-client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useDisconnect } from 'wagmi';
|
||||
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import {
|
||||
@ -19,7 +18,9 @@ import { getInitials } from 'utils/geInitials';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { cn } from 'utils/classnames';
|
||||
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 {
|
||||
mobileOpen?: boolean;
|
||||
@ -32,7 +33,6 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
||||
const isDesktop = useMediaQuery('(min-width: 960px)');
|
||||
|
||||
const [user, setUser] = useState<User>();
|
||||
const { disconnect } = useDisconnect();
|
||||
|
||||
const fetchUser = useCallback(async () => {
|
||||
const { user } = await client.getUser();
|
||||
@ -43,15 +43,54 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
||||
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 () => {
|
||||
await fetch(`${BASE_URL}/auth/logout`, {
|
||||
await fetch(`${baseUrl}/auth/logout`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
localStorage.clear();
|
||||
disconnect();
|
||||
navigate('/login');
|
||||
}, [disconnect, navigate]);
|
||||
}, [navigate]);
|
||||
|
||||
return (
|
||||
<motion.nav
|
||||
@ -75,8 +114,16 @@ export const Sidebar = ({ mobileOpen }: SidebarProps) => {
|
||||
<div className="hidden lg:flex">
|
||||
<Logo orgSlug={orgSlug} />
|
||||
</div>
|
||||
{/* This element ensures the space between logo and navigation */}
|
||||
<div className="flex-1"></div>
|
||||
{/* Switch organization */}
|
||||
<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 */}
|
||||
<div className="flex flex-col gap-5 justify-end">
|
||||
<Tabs defaultValue="Projects" orientation="vertical">
|
||||
|
@ -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 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 { 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) {
|
||||
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();
|
||||
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;
|
||||
}) {
|
||||
export default function Web3Provider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<WagmiProvider config={config}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</WagmiProvider>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
@ -14,34 +14,29 @@ import { GQLClientProvider } from './context/GQLClientContext';
|
||||
import { SERVER_GQL_PATH } from './constants';
|
||||
import { Toaster } from 'components/shared/Toast';
|
||||
import { LogErrorBoundary } from 'utils/log-error';
|
||||
import { BASE_URL } from 'utils/constants';
|
||||
import Web3ModalProvider from './context/Web3Provider';
|
||||
import { WalletConnectClientProvider } from 'context/WalletConnectContext';
|
||||
import { baseUrl } from 'utils/constants';
|
||||
|
||||
console.log(`v-0.0.9`);
|
||||
// @ts-ignore
|
||||
console.log(`v-${__VERSION__}`);
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement,
|
||||
);
|
||||
|
||||
assert(BASE_URL, 'VITE_SERVER_URL is not set in env');
|
||||
const gqlEndpoint = `${BASE_URL}/${SERVER_GQL_PATH}`;
|
||||
assert(baseUrl, 'VITE_SERVER_URL is not set in env');
|
||||
const gqlEndpoint = `${baseUrl}/${SERVER_GQL_PATH}`;
|
||||
|
||||
const gqlClient = new GQLClient({ gqlEndpoint });
|
||||
|
||||
root.render(
|
||||
<LogErrorBoundary>
|
||||
<React.StrictMode>
|
||||
<WalletConnectClientProvider>
|
||||
<ThemeProvider>
|
||||
<Web3ModalProvider>
|
||||
<GQLClientProvider client={gqlClient}>
|
||||
<App />
|
||||
<Toaster />
|
||||
</GQLClientProvider>
|
||||
</Web3ModalProvider>
|
||||
</ThemeProvider>
|
||||
</WalletConnectClientProvider>
|
||||
<ThemeProvider>
|
||||
<GQLClientProvider client={gqlClient}>
|
||||
<App />
|
||||
<Toaster />
|
||||
</GQLClientProvider>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
</LogErrorBoundary>,
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { CloudyFlow } from 'components/CloudyFlow';
|
||||
import { Login } from './auth/Login';
|
||||
import { SnowballAuth } from './auth/SnowballAuth';
|
||||
|
||||
const AuthPage = () => {
|
||||
return (
|
||||
@ -18,7 +18,7 @@ const AuthPage = () => {
|
||||
</div>
|
||||
<div className="pb-12 relative z-10 flex-1 flex-center">
|
||||
<div className="max-w-[520px] w-full bg-white rounded-xl shadow">
|
||||
<Login />
|
||||
<SnowballAuth />
|
||||
</div>
|
||||
</div>
|
||||
</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 { 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 (
|
||||
<div>
|
||||
<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" />
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
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',
|
||||
environment: Environment.Production,
|
||||
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,
|
||||
createdBy: {
|
||||
id: 'user1',
|
||||
@ -55,8 +48,6 @@ const deployment: Deployment = {
|
||||
},
|
||||
createdAt: '1677676800', // 2023-03-01T12:00:00Z
|
||||
updatedAt: '1677680400', // 2023-03-01T13:00:00Z
|
||||
applicationDeploymentRequestId:
|
||||
'bafyreiaycvq6imoppnpwdve4smj6t6ql5svt5zl3x6rimu4qwyzgjorize',
|
||||
};
|
||||
|
||||
const domains: Domain[] = [
|
||||
|
@ -26,7 +26,6 @@ const Projects = () => {
|
||||
return (
|
||||
<section className="px-4 md:px-6 py-6 flex flex-col gap-6">
|
||||
{/* Header */}
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="grow">
|
||||
<div className="flex gap-4 items-center">
|
||||
|
@ -92,13 +92,9 @@ const Id = () => {
|
||||
Open repo
|
||||
</Button>
|
||||
</Link>
|
||||
{(project.deployments.length > 0) &&
|
||||
<Link to={`https://${project.name.toLowerCase()}.${project.deployments[0].deployer.baseDomain}`}>
|
||||
<Button {...buttonSize} className="h-11 transition-colors">
|
||||
Go to app
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
<Button {...buttonSize} className="h-11 transition-colors">
|
||||
Go to app
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<WavyBorder />
|
||||
|
@ -31,11 +31,6 @@ const CreateWithTemplate = () => {
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
route: `/${orgSlug}/projects/create/template/configure`,
|
||||
label: 'Configure',
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
route: `/${orgSlug}/projects/create/template/deploy`,
|
||||
label: 'Deploy',
|
||||
},
|
||||
|
@ -2,8 +2,7 @@ import NewProject from './index';
|
||||
import CreateWithTemplate from './Template';
|
||||
import { templateRoutes } from './template/routes';
|
||||
import Id from './success/Id';
|
||||
import Configure from 'components/projects/create/Configure';
|
||||
import Deploy from 'components/projects/create/Deploy';
|
||||
import Import from './Import';
|
||||
|
||||
export const createProjectRoutes = [
|
||||
{
|
||||
@ -20,11 +19,7 @@ export const createProjectRoutes = [
|
||||
element: <Id />,
|
||||
},
|
||||
{
|
||||
path: 'configure',
|
||||
element: <Configure />,
|
||||
},
|
||||
{
|
||||
path: 'deploy',
|
||||
element: <Deploy />,
|
||||
path: 'import',
|
||||
element: <Import />,
|
||||
},
|
||||
];
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import Lottie from 'lottie-react';
|
||||
|
||||
import { Badge } from 'components/shared/Badge';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
ArrowLeftCircleFilledIcon,
|
||||
LinkChainIcon,
|
||||
QuestionMarkRoundFilledIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
@ -18,13 +19,15 @@ const Id = () => {
|
||||
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 () => {
|
||||
if (id) {
|
||||
// console.log('id', id);
|
||||
// console.log('getting project for id', id);
|
||||
const project = await client.getProject(id);
|
||||
// console.log('project found:', project);
|
||||
if (project && project.project) {
|
||||
// console.log('project:', project.project);
|
||||
setProject(project.project);
|
||||
}
|
||||
} else {
|
||||
@ -34,7 +37,7 @@ const Id = () => {
|
||||
|
||||
useEffect(() => {
|
||||
handleSetupDomain();
|
||||
}, []);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -48,10 +51,22 @@ const Id = () => {
|
||||
{/* Heading */}
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<Heading as="h3" className="font-medium text-xl">
|
||||
{isAuction
|
||||
? 'Auction created successfully.'
|
||||
: 'Project deployment created successfully.'}
|
||||
Project deployed successfully.
|
||||
</Heading>
|
||||
<p className="flex flex-col items-center lg:flex-row font-sans gap-0.5 lg:gap-2 text-sm text-elements-high-em">
|
||||
Your project has been deployed at{' '}
|
||||
<Button
|
||||
className="no-underline text-elements-link"
|
||||
// TODO: use dynamic value
|
||||
href={project ? `https://${project.subDomain}` : ''}
|
||||
as="a"
|
||||
variant="link-emphasized"
|
||||
external
|
||||
leftIcon={<LinkChainIcon />}
|
||||
>
|
||||
{project.subDomain}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
|
@ -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 { useOctokit } from '../../../../../context/OctokitContext';
|
||||
import { useGQLClient } from '../../../../../context/GQLClientContext';
|
||||
import { Template } from '../../../../../types/types';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { Input } from 'components/shared/Input';
|
||||
@ -14,6 +15,7 @@ import {
|
||||
ArrowRightCircleFilledIcon,
|
||||
LoadingIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { Checkbox } from 'components/shared/Checkbox';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
|
||||
@ -29,6 +31,7 @@ type SubmitRepoValues = {
|
||||
const CreateRepo = () => {
|
||||
const { octokit, isAuth } = useOctokit();
|
||||
const { template } = useOutletContext<{ template: Template }>();
|
||||
const client = useGQLClient();
|
||||
|
||||
const { orgSlug } = useParams();
|
||||
const { toast, dismiss } = useToast();
|
||||
@ -41,17 +44,6 @@ const CreateRepo = () => {
|
||||
const [gitAccounts, setGitAccounts] = useState<string[]>([]);
|
||||
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(
|
||||
async (data) => {
|
||||
assert(data.account);
|
||||
@ -61,22 +53,21 @@ const CreateRepo = () => {
|
||||
assert(template.repoFullName, 'Template URL not provided');
|
||||
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);
|
||||
|
||||
const { addProjectFromTemplate } = await client.addProjectFromTemplate(
|
||||
orgSlug!,
|
||||
{
|
||||
templateOwner: owner,
|
||||
templateRepo: repo,
|
||||
owner: data.account,
|
||||
name: data.repoName,
|
||||
isPrivate: false,
|
||||
},
|
||||
);
|
||||
|
||||
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) {
|
||||
setIsLoading(false);
|
||||
@ -104,7 +95,7 @@ const CreateRepo = () => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[octokit, toast],
|
||||
[octokit],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -185,12 +176,20 @@ const CreateRepo = () => {
|
||||
<Controller
|
||||
name="repoName"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Input value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
name="isPrivate"
|
||||
control={control}
|
||||
render={({}) => (
|
||||
<Checkbox label="Make this repo private" disabled={true} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
{...buttonSize}
|
||||
@ -204,7 +203,7 @@ const CreateRepo = () => {
|
||||
)
|
||||
}
|
||||
>
|
||||
Next
|
||||
Deploy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import CreateRepo from './index';
|
||||
import Configure from './Configure';
|
||||
import Deploy from './Deploy';
|
||||
|
||||
export const templateRoutes = [
|
||||
@ -7,10 +6,6 @@ export const templateRoutes = [
|
||||
index: true,
|
||||
element: <CreateRepo />,
|
||||
},
|
||||
{
|
||||
path: 'configure',
|
||||
element: <Configure />,
|
||||
},
|
||||
{
|
||||
path: 'deploy',
|
||||
element: <Deploy />,
|
||||
|
@ -21,7 +21,6 @@ import { Activity } from 'components/projects/project/overview/Activity';
|
||||
import { OverviewInfo } from 'components/projects/project/overview/OverviewInfo';
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
import { Domain, DomainStatus } from 'gql-client';
|
||||
import { AuctionCard } from 'components/projects/project/overview/Activity/AuctionCard';
|
||||
|
||||
const COMMITS_PER_PAGE = 4;
|
||||
|
||||
@ -129,19 +128,12 @@ const OverviewTabPanel = () => {
|
||||
<Heading className="text-lg leading-6 font-medium truncate">
|
||||
{project.name}
|
||||
</Heading>
|
||||
{project.deployments &&
|
||||
project.deployments.length > 0 &&
|
||||
project.deployments.map((deployment, index) => (
|
||||
<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>
|
||||
))}
|
||||
<a
|
||||
href={`https://${project.subDomain}`}
|
||||
className="text-sm text-elements-low-em tracking-tight truncate"
|
||||
>
|
||||
{project.subDomain}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<OverviewInfo label="Domain" icon={<GlobeIcon />}>
|
||||
@ -180,18 +172,14 @@ const OverviewTabPanel = () => {
|
||||
|
||||
{/* DEPLOYMENT */}
|
||||
<OverviewInfo label="Deployment URL" icon={<CursorBoxIcon />}>
|
||||
{project.deployments &&
|
||||
project.deployments.length > 0 &&
|
||||
project.deployments.map((deployment) => (
|
||||
<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">
|
||||
{`https://${project.name.toLowerCase()}.${deployment.deployer.baseDomain}`}
|
||||
{liveDomain?.name}{' '}
|
||||
<LinkIcon className="group-hover:rotate-45 transition-transform" />
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</OverviewInfo>
|
||||
|
||||
{/* DEPLOYMENT DATE */}
|
||||
@ -217,7 +205,6 @@ const OverviewTabPanel = () => {
|
||||
No current deployment found.
|
||||
</p>
|
||||
)}
|
||||
{project.auctionId && <AuctionCard project={project} />}
|
||||
</div>
|
||||
<Activity activities={activities} isLoading={fetchingActivities} />
|
||||
</div>
|
||||
|
@ -1,20 +1,22 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow';
|
||||
import DisplayEnvironmentVariables from 'components/projects/project/settings/DisplayEnvironmentVariables';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { EnvironmentVariablesFormValues } from '../../../../../types';
|
||||
import HorizontalLine from 'components/HorizontalLine';
|
||||
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 { InlineNotification } from 'components/shared/InlineNotification';
|
||||
import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { Environment, EnvironmentVariable } from 'gql-client';
|
||||
import EnvironmentVariablesForm from './EnvironmentVariablesForm';
|
||||
import { FieldValues, FormProvider, useForm } from 'react-hook-form';
|
||||
import { Button } from 'components/shared';
|
||||
|
||||
export const EnvironmentVariablesTabPanel = () => {
|
||||
const { id } = useParams();
|
||||
@ -25,9 +27,13 @@ export const EnvironmentVariablesTabPanel = () => {
|
||||
EnvironmentVariable[]
|
||||
>([]);
|
||||
|
||||
const [createNewVariable, setCreateNewVariable] = useState(false);
|
||||
|
||||
const methods = useForm<EnvironmentVariablesFormValues>({
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
control,
|
||||
reset,
|
||||
formState: { isSubmitSuccessful, errors },
|
||||
} = useForm<EnvironmentVariablesFormValues>({
|
||||
defaultValues: {
|
||||
variables: [{ key: '', value: '' }],
|
||||
environment: {
|
||||
@ -37,6 +43,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(
|
||||
(environment: Environment) => {
|
||||
@ -47,6 +68,21 @@ export const EnvironmentVariablesTabPanel = () => {
|
||||
[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(
|
||||
async (id: string | undefined) => {
|
||||
if (id) {
|
||||
@ -63,24 +99,22 @@ export const EnvironmentVariablesTabPanel = () => {
|
||||
}, [id]);
|
||||
|
||||
const createEnvironmentVariablesHandler = useCallback(
|
||||
async (createFormData: FieldValues) => {
|
||||
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)),
|
||||
};
|
||||
},
|
||||
);
|
||||
async (createFormData: EnvironmentVariablesFormValues) => {
|
||||
const environmentVariables = createFormData.variables.map((variable) => {
|
||||
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 { addEnvironmentVariables: isEnvironmentVariablesAdded } =
|
||||
await client.addEnvironmentVariables(id!, environmentVariables);
|
||||
|
||||
if (isEnvironmentVariablesAdded) {
|
||||
methods.reset();
|
||||
reset();
|
||||
setCreateNewVariable((cur) => !cur);
|
||||
|
||||
fetchEnvironmentVariables(id);
|
||||
@ -125,14 +159,59 @@ export const EnvironmentVariablesTabPanel = () => {
|
||||
</div>
|
||||
</Heading>
|
||||
<Collapse open={createNewVariable}>
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
onSubmit={methods.handleSubmit((data) =>
|
||||
createEnvironmentVariablesHandler(data),
|
||||
<div className="p-4 bg-slate-100">
|
||||
<form onSubmit={handleSubmit(createEnvironmentVariablesHandler)}>
|
||||
{fields.map((field, index) => {
|
||||
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="p-4 bg-slate-100">
|
||||
<EnvironmentVariablesForm />
|
||||
<div className="flex gap-2 p-2">
|
||||
<Checkbox
|
||||
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 className="p-2">
|
||||
<Button size="md" type="submit">
|
||||
@ -140,7 +219,7 @@ export const EnvironmentVariablesTabPanel = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
<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;
|
@ -102,15 +102,6 @@ export const deployment0: Deployment = {
|
||||
domain: domain0,
|
||||
commitMessage: 'Commit Message',
|
||||
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 = {
|
||||
@ -128,20 +119,7 @@ export const project: Project = {
|
||||
organization: organization,
|
||||
template: 'Template',
|
||||
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'],
|
||||
icon: 'Icon',
|
||||
fundsReleased: true,
|
||||
baseDomains: ['baseDomain'],
|
||||
subDomain: 'SubDomain',
|
||||
};
|
||||
|
@ -17,7 +17,7 @@ const meta: Meta<typeof AddEnvironmentVariableRow> = {
|
||||
pathParams: { userId: 'me' },
|
||||
},
|
||||
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' },
|
||||
},
|
||||
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' },
|
||||
},
|
||||
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 SetupDomain> = {
|
||||
pathParams: { userId: 'me' },
|
||||
},
|
||||
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,9 +1,9 @@
|
||||
import { BASE_URL } from './constants';
|
||||
import { baseUrl } from './constants';
|
||||
|
||||
export async function verifyAccessCode(
|
||||
accesscode: string,
|
||||
): Promise<boolean | null> {
|
||||
const res = await fetch(`${BASE_URL}/auth/accesscode`, {
|
||||
const res = await fetch(`${baseUrl}/auth/accesscode`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
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 TURNKEY_BASE_URL = import.meta.env.VITE_TURNKEY_API_BASE_URL!;
|
||||
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_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_LACONICD_CHAIN_ID = import.meta.env.VITE_LACONICD_CHAIN_ID;
|
||||
|
@ -1,36 +1,34 @@
|
||||
// import React from 'react';
|
||||
// import Bugsnag from '@bugsnag/js';
|
||||
// import BugsnagPluginReact from '@bugsnag/plugin-react';
|
||||
// import BugsnagPerformance from '@bugsnag/browser-performance';
|
||||
import React from 'react';
|
||||
import Bugsnag from '@bugsnag/js';
|
||||
import BugsnagPluginReact from '@bugsnag/plugin-react';
|
||||
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) {
|
||||
// Bugsnag.start({
|
||||
// apiKey: VITE_BUGSNAG_API_KEY,
|
||||
// plugins: [new BugsnagPluginReact()],
|
||||
// });
|
||||
// BugsnagPerformance.start({ apiKey: VITE_BUGSNAG_API_KEY });
|
||||
// }
|
||||
if (VITE_BUGSNAG_API_KEY) {
|
||||
Bugsnag.start({
|
||||
apiKey: VITE_BUGSNAG_API_KEY,
|
||||
plugins: [new BugsnagPluginReact()],
|
||||
});
|
||||
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
|
||||
// ? Bugsnag.getPlugin('react')!.createErrorBoundary(React)
|
||||
// : ({ children }: any) => children;
|
||||
export const LogErrorBoundary = VITE_BUGSNAG_API_KEY
|
||||
? Bugsnag.getPlugin('react')!.createErrorBoundary(React)
|
||||
: ({ children }: any) => children;
|
||||
|
||||
// export function logError(error: Error) {
|
||||
// let errors: any[] = [error];
|
||||
// let safety = 0;
|
||||
// while (errors[errors.length - 1].cause && safety < 10) {
|
||||
// errors.push('::caused by::', errors[errors.length - 1].cause);
|
||||
// safety += 1;
|
||||
// }
|
||||
// console.error(...errors);
|
||||
export function logError(error: Error) {
|
||||
let errors: any[] = [error];
|
||||
let safety = 0;
|
||||
while (errors[errors.length - 1].cause && safety < 10) {
|
||||
errors.push('::caused by::', errors[errors.length - 1].cause);
|
||||
safety += 1;
|
||||
}
|
||||
console.error(...errors);
|
||||
|
||||
// if (VITE_BUGSNAG_API_KEY) {
|
||||
// Bugsnag.notify(error);
|
||||
// }
|
||||
// }
|
||||
|
||||
export const LogErrorBoundary = ({ children }: any) => children;
|
||||
if (VITE_BUGSNAG_API_KEY) {
|
||||
Bugsnag.notify(error);
|
||||
}
|
||||
}
|
||||
|
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();
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { TurnkeyClient, getWebAuthnAttestation } from '@turnkey/http';
|
||||
import { WebauthnStamper } from '@turnkey/webauthn-stamper';
|
||||
|
||||
import { BASE_URL, PASSKEY_WALLET_RPID, TURNKEY_BASE_URL } from './constants';
|
||||
import { baseUrl, PASSKEY_WALLET_RPID, TURNKEY_BASE_URL } from './constants';
|
||||
|
||||
// All algorithms can be found here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms
|
||||
// We only support ES256, which is listed here
|
||||
@ -10,7 +10,7 @@ const es256 = -7;
|
||||
export async function subOrganizationIdForEmail(
|
||||
email: string,
|
||||
): Promise<string | null> {
|
||||
const res = await fetch(`${BASE_URL}/auth/registration/${email}`);
|
||||
const res = await fetch(`${baseUrl}/auth/registration/${email}`);
|
||||
|
||||
// If API returns a non-empty 200, this email maps to an existing user.
|
||||
if (res.status == 200) {
|
||||
@ -64,7 +64,7 @@ export async function turnkeySignup(email: string) {
|
||||
},
|
||||
});
|
||||
|
||||
const res = await fetch(`${BASE_URL}/auth/register`, {
|
||||
const res = await fetch(`${baseUrl}/auth/register`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
@ -108,7 +108,7 @@ export async function turnkeySignin(subOrganizationId: string) {
|
||||
throw new Error(`Error during webauthn prompt: ${e}`);
|
||||
}
|
||||
|
||||
const res = await fetch(`${BASE_URL}/auth/authenticate`, {
|
||||
const res = await fetch(`${baseUrl}/auth/authenticate`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
signedWhoamiRequest: signedRequest,
|
||||
|
33
packages/frontend/src/utils/use-snowball.ts
Normal file
33
packages/frontend/src/utils/use-snowball.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Snowball, SnowballChain } from '@snowballtools/js-sdk';
|
||||
import {
|
||||
// LitAppleAuth,
|
||||
LitGoogleAuth,
|
||||
LitPasskeyAuth,
|
||||
} from '@snowballtools/auth-lit';
|
||||
import { VITE_LIT_RELAY_API_KEY } from './constants';
|
||||
|
||||
export const snowball = Snowball.withAuth({
|
||||
google: LitGoogleAuth.configure({
|
||||
litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
|
||||
}),
|
||||
// apple: LitAppleAuth.configure({
|
||||
// litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
|
||||
// }),
|
||||
passkey: LitPasskeyAuth.configure({
|
||||
litRelayApiKey: VITE_LIT_RELAY_API_KEY!,
|
||||
}),
|
||||
}).create({
|
||||
initialChain: SnowballChain.sepolia,
|
||||
});
|
||||
|
||||
export function useSnowball() {
|
||||
const [state, setState] = useState(100);
|
||||
|
||||
useEffect(() => {
|
||||
// Subscribe and directly return the unsubscribe function
|
||||
return snowball.subscribe(() => setState(state + 1));
|
||||
}, [state]);
|
||||
|
||||
return snowball;
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { WalletConnectModal } from '@walletconnect/modal';
|
||||
|
||||
import { VITE_WALLET_CONNECT_ID } from 'utils/constants';
|
||||
|
||||
export const walletConnectModal = new WalletConnectModal({
|
||||
projectId: VITE_WALLET_CONNECT_ID,
|
||||
chains: [],
|
||||
});
|
@ -13,6 +13,7 @@ export default defineConfig({
|
||||
context: '/src/context',
|
||||
components: '/src/components',
|
||||
pages: '/src/pages',
|
||||
types: '/src/types',
|
||||
},
|
||||
},
|
||||
define: {
|
||||
|
2
packages/gql-client/.gitignore
vendored
2
packages/gql-client/.gitignore
vendored
@ -1 +1 @@
|
||||
dist
|
||||
# dist
|
||||
|
295
packages/gql-client/dist/index.d.mts
vendored
Normal file
295
packages/gql-client/dist/index.d.mts
vendored
Normal file
@ -0,0 +1,295 @@
|
||||
declare enum Role {
|
||||
Owner = "Owner",
|
||||
Maintainer = "Maintainer",
|
||||
Reader = "Reader"
|
||||
}
|
||||
declare enum Permission {
|
||||
View = "View",
|
||||
Edit = "Edit"
|
||||
}
|
||||
declare enum Environment {
|
||||
Production = "Production",
|
||||
Preview = "Preview",
|
||||
Development = "Development"
|
||||
}
|
||||
declare enum DeploymentStatus {
|
||||
Building = "Building",
|
||||
Ready = "Ready",
|
||||
Error = "Error",
|
||||
Deleting = "Deleting"
|
||||
}
|
||||
declare enum DomainStatus {
|
||||
Live = "Live",
|
||||
Pending = "Pending"
|
||||
}
|
||||
type EnvironmentVariable = {
|
||||
id: string;
|
||||
environment: Environment;
|
||||
key: string;
|
||||
value: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
type Domain = {
|
||||
id: string;
|
||||
branch: string;
|
||||
name: string;
|
||||
status: DomainStatus;
|
||||
redirectTo: Domain | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
type User = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
isVerified: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
gitHubToken: string | null;
|
||||
};
|
||||
type Deployment = {
|
||||
id: string;
|
||||
domain: Domain;
|
||||
branch: string;
|
||||
commitHash: string;
|
||||
commitMessage: string;
|
||||
url?: string;
|
||||
environment: Environment;
|
||||
isCurrent: boolean;
|
||||
status: DeploymentStatus;
|
||||
createdBy: User;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
type OrganizationMember = {
|
||||
id: string;
|
||||
member: User;
|
||||
role: Role;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
type ProjectMember = {
|
||||
id: string;
|
||||
member: User;
|
||||
permissions: Permission[];
|
||||
isPending: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
type OrganizationProject = {
|
||||
id: string;
|
||||
owner: User;
|
||||
deployments: Deployment[];
|
||||
name: string;
|
||||
repository: string;
|
||||
prodBranch: string;
|
||||
description: string;
|
||||
template: string;
|
||||
framework: string;
|
||||
webhooks: string[];
|
||||
members: ProjectMember[];
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
type Organization = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
projects: OrganizationProject[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
members: OrganizationMember[];
|
||||
};
|
||||
type Project = {
|
||||
id: string;
|
||||
owner: User;
|
||||
deployments: Deployment[];
|
||||
name: string;
|
||||
repository: string;
|
||||
prodBranch: string;
|
||||
description: string;
|
||||
template: string;
|
||||
framework: string;
|
||||
webhooks: string[];
|
||||
members: ProjectMember[];
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
organization: Organization;
|
||||
icon: string;
|
||||
subDomain: string;
|
||||
};
|
||||
type GetProjectMembersResponse = {
|
||||
projectMembers: ProjectMember[];
|
||||
};
|
||||
type AddProjectMemberResponse = {
|
||||
addProjectMember: boolean;
|
||||
};
|
||||
type RemoveProjectMemberResponse = {
|
||||
removeProjectMember: boolean;
|
||||
};
|
||||
type UpdateProjectMemberResponse = {
|
||||
updateProjectMember: boolean;
|
||||
};
|
||||
type GetDeploymentsResponse = {
|
||||
deployments: Deployment[];
|
||||
};
|
||||
type GetEnvironmentVariablesResponse = {
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
};
|
||||
type GetOrganizationsResponse = {
|
||||
organizations: Organization[];
|
||||
};
|
||||
type GetUserResponse = {
|
||||
user: User;
|
||||
};
|
||||
type GetProjectResponse = {
|
||||
project: Project | null;
|
||||
};
|
||||
type GetProjectsInOrganizationResponse = {
|
||||
projectsInOrganization: Project[];
|
||||
};
|
||||
type GetDomainsResponse = {
|
||||
domains: Domain[];
|
||||
};
|
||||
type SearchProjectsResponse = {
|
||||
searchProjects: Project[];
|
||||
};
|
||||
type AddEnvironmentVariablesResponse = {
|
||||
addEnvironmentVariables: boolean;
|
||||
};
|
||||
type AddEnvironmentVariableInput = {
|
||||
environments: string[];
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
type UpdateEnvironmentVariableInput = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
type UpdateProjectMemberInput = {
|
||||
permissions: Permission[];
|
||||
};
|
||||
type AddProjectMemberInput = {
|
||||
email: string;
|
||||
permissions: Permission[];
|
||||
};
|
||||
type UpdateEnvironmentVariableResponse = {
|
||||
updateEnvironmentVariable: boolean;
|
||||
};
|
||||
type RemoveEnvironmentVariableResponse = {
|
||||
removeEnvironmentVariable: boolean;
|
||||
};
|
||||
type UpdateDeploymentToProdResponse = {
|
||||
updateDeploymentToProd: boolean;
|
||||
};
|
||||
type AddProjectFromTemplateResponse = {
|
||||
addProjectFromTemplate: Project;
|
||||
};
|
||||
type AddProjectResponse = {
|
||||
addProject: Project;
|
||||
};
|
||||
type UpdateProjectResponse = {
|
||||
updateProject: boolean;
|
||||
};
|
||||
type UpdateDomainResponse = {
|
||||
updateDomain: boolean;
|
||||
};
|
||||
type DeleteProjectResponse = {
|
||||
deleteProject: boolean;
|
||||
};
|
||||
type DeleteDomainResponse = {
|
||||
deleteDomain: boolean;
|
||||
};
|
||||
type AddProjectFromTemplateInput = {
|
||||
templateOwner: string;
|
||||
templateRepo: string;
|
||||
owner: string;
|
||||
name: string;
|
||||
isPrivate: boolean;
|
||||
};
|
||||
type AddProjectInput = {
|
||||
name: string;
|
||||
repository: string;
|
||||
prodBranch: string;
|
||||
template?: string;
|
||||
};
|
||||
type UpdateProjectInput = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
prodBranch?: string;
|
||||
webhooks?: string[];
|
||||
organizationId?: string;
|
||||
};
|
||||
type UpdateDomainInput = {
|
||||
name?: string;
|
||||
branch?: string;
|
||||
redirectToId?: string | null;
|
||||
};
|
||||
type RedeployToProdResponse = {
|
||||
redeployToProd: boolean;
|
||||
};
|
||||
type RollbackDeploymentResponse = {
|
||||
rollbackDeployment: boolean;
|
||||
};
|
||||
type DeleteDeploymentResponse = {
|
||||
deleteDeployment: boolean;
|
||||
};
|
||||
type AddDomainInput = {
|
||||
name: string;
|
||||
};
|
||||
type FilterDomainInput = {
|
||||
branch?: string;
|
||||
status?: DomainStatus;
|
||||
};
|
||||
type AddDomainResponse = {
|
||||
addDomain: true;
|
||||
};
|
||||
type AuthenticateGitHubResponse = {
|
||||
authenticateGitHub: {
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
type UnauthenticateGitHubResponse = {
|
||||
unauthenticateGitHub: boolean;
|
||||
};
|
||||
|
||||
interface GraphQLConfig {
|
||||
gqlEndpoint: string;
|
||||
}
|
||||
declare class GQLClient {
|
||||
private client;
|
||||
constructor(config: GraphQLConfig);
|
||||
getUser(): Promise<GetUserResponse>;
|
||||
getProject(projectId: string): Promise<GetProjectResponse>;
|
||||
getProjectsInOrganization(organizationSlug: string): Promise<GetProjectsInOrganizationResponse>;
|
||||
getOrganizations(): Promise<GetOrganizationsResponse>;
|
||||
getDeployments(projectId: string): Promise<GetDeploymentsResponse>;
|
||||
getEnvironmentVariables(projectId: string): Promise<GetEnvironmentVariablesResponse>;
|
||||
getProjectMembers(projectId: string): Promise<GetProjectMembersResponse>;
|
||||
addProjectMember(projectId: string, data: AddProjectMemberInput): Promise<AddProjectMemberResponse>;
|
||||
updateProjectMember(projectMemberId: string, data: UpdateProjectMemberInput): Promise<UpdateProjectMemberResponse>;
|
||||
removeProjectMember(projectMemberId: string): Promise<RemoveProjectMemberResponse>;
|
||||
searchProjects(searchText: string): Promise<SearchProjectsResponse>;
|
||||
addEnvironmentVariables(projectId: string, data: AddEnvironmentVariableInput[]): Promise<AddEnvironmentVariablesResponse>;
|
||||
updateEnvironmentVariable(environmentVariableId: string, data: UpdateEnvironmentVariableInput): Promise<UpdateEnvironmentVariableResponse>;
|
||||
removeEnvironmentVariable(environmentVariableId: string): Promise<RemoveEnvironmentVariableResponse>;
|
||||
updateDeploymentToProd(deploymentId: string): Promise<UpdateDeploymentToProdResponse>;
|
||||
addProjectFromTemplate(organizationSlug: string, data: AddProjectFromTemplateInput): Promise<AddProjectFromTemplateResponse>;
|
||||
addProject(organizationSlug: string, data: AddProjectInput): Promise<AddProjectResponse>;
|
||||
updateProject(projectId: string, data: UpdateProjectInput): Promise<UpdateProjectResponse>;
|
||||
updateDomain(domainId: string, data: UpdateDomainInput): Promise<UpdateDomainResponse>;
|
||||
redeployToProd(deploymentId: string): Promise<RedeployToProdResponse>;
|
||||
deleteProject(projectId: string): Promise<DeleteProjectResponse>;
|
||||
deleteDomain(domainId: string): Promise<DeleteDomainResponse>;
|
||||
rollbackDeployment(projectId: string, deploymentId: string): Promise<RollbackDeploymentResponse>;
|
||||
deleteDeployment(deploymentId: string): Promise<DeleteDeploymentResponse>;
|
||||
addDomain(projectId: string, data: AddDomainInput): Promise<AddDomainResponse>;
|
||||
getDomains(projectId: string, filter?: FilterDomainInput): Promise<GetDomainsResponse>;
|
||||
authenticateGitHub(code: string): Promise<AuthenticateGitHubResponse>;
|
||||
unauthenticateGithub(): Promise<UnauthenticateGitHubResponse>;
|
||||
}
|
||||
|
||||
export { type AddDomainInput, type AddDomainResponse, type AddEnvironmentVariableInput, type AddEnvironmentVariablesResponse, type AddProjectFromTemplateInput, type AddProjectFromTemplateResponse, type AddProjectInput, type AddProjectMemberInput, type AddProjectMemberResponse, type AddProjectResponse, type AuthenticateGitHubResponse, type DeleteDeploymentResponse, type DeleteDomainResponse, type DeleteProjectResponse, type Deployment, DeploymentStatus, type Domain, DomainStatus, Environment, type EnvironmentVariable, type FilterDomainInput, GQLClient, type GetDeploymentsResponse, type GetDomainsResponse, type GetEnvironmentVariablesResponse, type GetOrganizationsResponse, type GetProjectMembersResponse, type GetProjectResponse, type GetProjectsInOrganizationResponse, type GetUserResponse, type GraphQLConfig, type Organization, type OrganizationMember, type OrganizationProject, Permission, type Project, type ProjectMember, type RedeployToProdResponse, type RemoveEnvironmentVariableResponse, type RemoveProjectMemberResponse, Role, type RollbackDeploymentResponse, type SearchProjectsResponse, type UnauthenticateGitHubResponse, type UpdateDeploymentToProdResponse, type UpdateDomainInput, type UpdateDomainResponse, type UpdateEnvironmentVariableInput, type UpdateEnvironmentVariableResponse, type UpdateProjectInput, type UpdateProjectMemberInput, type UpdateProjectMemberResponse, type UpdateProjectResponse, type User };
|
295
packages/gql-client/dist/index.d.ts
vendored
Normal file
295
packages/gql-client/dist/index.d.ts
vendored
Normal file
@ -0,0 +1,295 @@
|
||||
declare enum Role {
|
||||
Owner = "Owner",
|
||||
Maintainer = "Maintainer",
|
||||
Reader = "Reader"
|
||||
}
|
||||
declare enum Permission {
|
||||
View = "View",
|
||||
Edit = "Edit"
|
||||
}
|
||||
declare enum Environment {
|
||||
Production = "Production",
|
||||
Preview = "Preview",
|
||||
Development = "Development"
|
||||
}
|
||||
declare enum DeploymentStatus {
|
||||
Building = "Building",
|
||||
Ready = "Ready",
|
||||
Error = "Error",
|
||||
Deleting = "Deleting"
|
||||
}
|
||||
declare enum DomainStatus {
|
||||
Live = "Live",
|
||||
Pending = "Pending"
|
||||
}
|
||||
type EnvironmentVariable = {
|
||||
id: string;
|
||||
environment: Environment;
|
||||
key: string;
|
||||
value: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
type Domain = {
|
||||
id: string;
|
||||
branch: string;
|
||||
name: string;
|
||||
status: DomainStatus;
|
||||
redirectTo: Domain | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
type User = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
isVerified: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
gitHubToken: string | null;
|
||||
};
|
||||
type Deployment = {
|
||||
id: string;
|
||||
domain: Domain;
|
||||
branch: string;
|
||||
commitHash: string;
|
||||
commitMessage: string;
|
||||
url?: string;
|
||||
environment: Environment;
|
||||
isCurrent: boolean;
|
||||
status: DeploymentStatus;
|
||||
createdBy: User;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
type OrganizationMember = {
|
||||
id: string;
|
||||
member: User;
|
||||
role: Role;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
type ProjectMember = {
|
||||
id: string;
|
||||
member: User;
|
||||
permissions: Permission[];
|
||||
isPending: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
type OrganizationProject = {
|
||||
id: string;
|
||||
owner: User;
|
||||
deployments: Deployment[];
|
||||
name: string;
|
||||
repository: string;
|
||||
prodBranch: string;
|
||||
description: string;
|
||||
template: string;
|
||||
framework: string;
|
||||
webhooks: string[];
|
||||
members: ProjectMember[];
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
type Organization = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
projects: OrganizationProject[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
members: OrganizationMember[];
|
||||
};
|
||||
type Project = {
|
||||
id: string;
|
||||
owner: User;
|
||||
deployments: Deployment[];
|
||||
name: string;
|
||||
repository: string;
|
||||
prodBranch: string;
|
||||
description: string;
|
||||
template: string;
|
||||
framework: string;
|
||||
webhooks: string[];
|
||||
members: ProjectMember[];
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
organization: Organization;
|
||||
icon: string;
|
||||
subDomain: string;
|
||||
};
|
||||
type GetProjectMembersResponse = {
|
||||
projectMembers: ProjectMember[];
|
||||
};
|
||||
type AddProjectMemberResponse = {
|
||||
addProjectMember: boolean;
|
||||
};
|
||||
type RemoveProjectMemberResponse = {
|
||||
removeProjectMember: boolean;
|
||||
};
|
||||
type UpdateProjectMemberResponse = {
|
||||
updateProjectMember: boolean;
|
||||
};
|
||||
type GetDeploymentsResponse = {
|
||||
deployments: Deployment[];
|
||||
};
|
||||
type GetEnvironmentVariablesResponse = {
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
};
|
||||
type GetOrganizationsResponse = {
|
||||
organizations: Organization[];
|
||||
};
|
||||
type GetUserResponse = {
|
||||
user: User;
|
||||
};
|
||||
type GetProjectResponse = {
|
||||
project: Project | null;
|
||||
};
|
||||
type GetProjectsInOrganizationResponse = {
|
||||
projectsInOrganization: Project[];
|
||||
};
|
||||
type GetDomainsResponse = {
|
||||
domains: Domain[];
|
||||
};
|
||||
type SearchProjectsResponse = {
|
||||
searchProjects: Project[];
|
||||
};
|
||||
type AddEnvironmentVariablesResponse = {
|
||||
addEnvironmentVariables: boolean;
|
||||
};
|
||||
type AddEnvironmentVariableInput = {
|
||||
environments: string[];
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
type UpdateEnvironmentVariableInput = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
type UpdateProjectMemberInput = {
|
||||
permissions: Permission[];
|
||||
};
|
||||
type AddProjectMemberInput = {
|
||||
email: string;
|
||||
permissions: Permission[];
|
||||
};
|
||||
type UpdateEnvironmentVariableResponse = {
|
||||
updateEnvironmentVariable: boolean;
|
||||
};
|
||||
type RemoveEnvironmentVariableResponse = {
|
||||
removeEnvironmentVariable: boolean;
|
||||
};
|
||||
type UpdateDeploymentToProdResponse = {
|
||||
updateDeploymentToProd: boolean;
|
||||
};
|
||||
type AddProjectFromTemplateResponse = {
|
||||
addProjectFromTemplate: Project;
|
||||
};
|
||||
type AddProjectResponse = {
|
||||
addProject: Project;
|
||||
};
|
||||
type UpdateProjectResponse = {
|
||||
updateProject: boolean;
|
||||
};
|
||||
type UpdateDomainResponse = {
|
||||
updateDomain: boolean;
|
||||
};
|
||||
type DeleteProjectResponse = {
|
||||
deleteProject: boolean;
|
||||
};
|
||||
type DeleteDomainResponse = {
|
||||
deleteDomain: boolean;
|
||||
};
|
||||
type AddProjectFromTemplateInput = {
|
||||
templateOwner: string;
|
||||
templateRepo: string;
|
||||
owner: string;
|
||||
name: string;
|
||||
isPrivate: boolean;
|
||||
};
|
||||
type AddProjectInput = {
|
||||
name: string;
|
||||
repository: string;
|
||||
prodBranch: string;
|
||||
template?: string;
|
||||
};
|
||||
type UpdateProjectInput = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
prodBranch?: string;
|
||||
webhooks?: string[];
|
||||
organizationId?: string;
|
||||
};
|
||||
type UpdateDomainInput = {
|
||||
name?: string;
|
||||
branch?: string;
|
||||
redirectToId?: string | null;
|
||||
};
|
||||
type RedeployToProdResponse = {
|
||||
redeployToProd: boolean;
|
||||
};
|
||||
type RollbackDeploymentResponse = {
|
||||
rollbackDeployment: boolean;
|
||||
};
|
||||
type DeleteDeploymentResponse = {
|
||||
deleteDeployment: boolean;
|
||||
};
|
||||
type AddDomainInput = {
|
||||
name: string;
|
||||
};
|
||||
type FilterDomainInput = {
|
||||
branch?: string;
|
||||
status?: DomainStatus;
|
||||
};
|
||||
type AddDomainResponse = {
|
||||
addDomain: true;
|
||||
};
|
||||
type AuthenticateGitHubResponse = {
|
||||
authenticateGitHub: {
|
||||
token: string;
|
||||
};
|
||||
};
|
||||
type UnauthenticateGitHubResponse = {
|
||||
unauthenticateGitHub: boolean;
|
||||
};
|
||||
|
||||
interface GraphQLConfig {
|
||||
gqlEndpoint: string;
|
||||
}
|
||||
declare class GQLClient {
|
||||
private client;
|
||||
constructor(config: GraphQLConfig);
|
||||
getUser(): Promise<GetUserResponse>;
|
||||
getProject(projectId: string): Promise<GetProjectResponse>;
|
||||
getProjectsInOrganization(organizationSlug: string): Promise<GetProjectsInOrganizationResponse>;
|
||||
getOrganizations(): Promise<GetOrganizationsResponse>;
|
||||
getDeployments(projectId: string): Promise<GetDeploymentsResponse>;
|
||||
getEnvironmentVariables(projectId: string): Promise<GetEnvironmentVariablesResponse>;
|
||||
getProjectMembers(projectId: string): Promise<GetProjectMembersResponse>;
|
||||
addProjectMember(projectId: string, data: AddProjectMemberInput): Promise<AddProjectMemberResponse>;
|
||||
updateProjectMember(projectMemberId: string, data: UpdateProjectMemberInput): Promise<UpdateProjectMemberResponse>;
|
||||
removeProjectMember(projectMemberId: string): Promise<RemoveProjectMemberResponse>;
|
||||
searchProjects(searchText: string): Promise<SearchProjectsResponse>;
|
||||
addEnvironmentVariables(projectId: string, data: AddEnvironmentVariableInput[]): Promise<AddEnvironmentVariablesResponse>;
|
||||
updateEnvironmentVariable(environmentVariableId: string, data: UpdateEnvironmentVariableInput): Promise<UpdateEnvironmentVariableResponse>;
|
||||
removeEnvironmentVariable(environmentVariableId: string): Promise<RemoveEnvironmentVariableResponse>;
|
||||
updateDeploymentToProd(deploymentId: string): Promise<UpdateDeploymentToProdResponse>;
|
||||
addProjectFromTemplate(organizationSlug: string, data: AddProjectFromTemplateInput): Promise<AddProjectFromTemplateResponse>;
|
||||
addProject(organizationSlug: string, data: AddProjectInput): Promise<AddProjectResponse>;
|
||||
updateProject(projectId: string, data: UpdateProjectInput): Promise<UpdateProjectResponse>;
|
||||
updateDomain(domainId: string, data: UpdateDomainInput): Promise<UpdateDomainResponse>;
|
||||
redeployToProd(deploymentId: string): Promise<RedeployToProdResponse>;
|
||||
deleteProject(projectId: string): Promise<DeleteProjectResponse>;
|
||||
deleteDomain(domainId: string): Promise<DeleteDomainResponse>;
|
||||
rollbackDeployment(projectId: string, deploymentId: string): Promise<RollbackDeploymentResponse>;
|
||||
deleteDeployment(deploymentId: string): Promise<DeleteDeploymentResponse>;
|
||||
addDomain(projectId: string, data: AddDomainInput): Promise<AddDomainResponse>;
|
||||
getDomains(projectId: string, filter?: FilterDomainInput): Promise<GetDomainsResponse>;
|
||||
authenticateGitHub(code: string): Promise<AuthenticateGitHubResponse>;
|
||||
unauthenticateGithub(): Promise<UnauthenticateGitHubResponse>;
|
||||
}
|
||||
|
||||
export { type AddDomainInput, type AddDomainResponse, type AddEnvironmentVariableInput, type AddEnvironmentVariablesResponse, type AddProjectFromTemplateInput, type AddProjectFromTemplateResponse, type AddProjectInput, type AddProjectMemberInput, type AddProjectMemberResponse, type AddProjectResponse, type AuthenticateGitHubResponse, type DeleteDeploymentResponse, type DeleteDomainResponse, type DeleteProjectResponse, type Deployment, DeploymentStatus, type Domain, DomainStatus, Environment, type EnvironmentVariable, type FilterDomainInput, GQLClient, type GetDeploymentsResponse, type GetDomainsResponse, type GetEnvironmentVariablesResponse, type GetOrganizationsResponse, type GetProjectMembersResponse, type GetProjectResponse, type GetProjectsInOrganizationResponse, type GetUserResponse, type GraphQLConfig, type Organization, type OrganizationMember, type OrganizationProject, Permission, type Project, type ProjectMember, type RedeployToProdResponse, type RemoveEnvironmentVariableResponse, type RemoveProjectMemberResponse, Role, type RollbackDeploymentResponse, type SearchProjectsResponse, type UnauthenticateGitHubResponse, type UpdateDeploymentToProdResponse, type UpdateDomainInput, type UpdateDomainResponse, type UpdateEnvironmentVariableInput, type UpdateEnvironmentVariableResponse, type UpdateProjectInput, type UpdateProjectMemberInput, type UpdateProjectMemberResponse, type UpdateProjectResponse, type User };
|
748
packages/gql-client/dist/index.js
vendored
Normal file
748
packages/gql-client/dist/index.js
vendored
Normal file
@ -0,0 +1,748 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var __async = (__this, __arguments, generator) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
var fulfilled = (value) => {
|
||||
try {
|
||||
step(generator.next(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var rejected = (value) => {
|
||||
try {
|
||||
step(generator.throw(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
||||
step((generator = generator.apply(__this, __arguments)).next());
|
||||
});
|
||||
};
|
||||
|
||||
// src/index.ts
|
||||
var src_exports = {};
|
||||
__export(src_exports, {
|
||||
DeploymentStatus: () => DeploymentStatus,
|
||||
DomainStatus: () => DomainStatus,
|
||||
Environment: () => Environment,
|
||||
GQLClient: () => GQLClient,
|
||||
Permission: () => Permission,
|
||||
Role: () => Role
|
||||
});
|
||||
module.exports = __toCommonJS(src_exports);
|
||||
|
||||
// src/client.ts
|
||||
var import_client3 = require("@apollo/client");
|
||||
|
||||
// src/queries.ts
|
||||
var import_client = require("@apollo/client");
|
||||
var getUser = import_client.gql`
|
||||
query {
|
||||
user {
|
||||
id
|
||||
name
|
||||
email
|
||||
createdAt
|
||||
updatedAt
|
||||
gitHubToken
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getProject = import_client.gql`
|
||||
query ($projectId: String!) {
|
||||
project(projectId: $projectId) {
|
||||
createdAt
|
||||
description
|
||||
id
|
||||
name
|
||||
template
|
||||
updatedAt
|
||||
prodBranch
|
||||
framework
|
||||
repository
|
||||
webhooks
|
||||
icon
|
||||
subDomain
|
||||
organization {
|
||||
id
|
||||
name
|
||||
}
|
||||
owner {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
deployments {
|
||||
id
|
||||
branch
|
||||
isCurrent
|
||||
status
|
||||
updatedAt
|
||||
commitHash
|
||||
createdAt
|
||||
environment
|
||||
domain {
|
||||
status
|
||||
branch
|
||||
createdAt
|
||||
updatedAt
|
||||
id
|
||||
name
|
||||
}
|
||||
createdBy {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getProjectsInOrganization = import_client.gql`
|
||||
query ($organizationSlug: String!) {
|
||||
projectsInOrganization(organizationSlug: $organizationSlug) {
|
||||
id
|
||||
name
|
||||
createdAt
|
||||
description
|
||||
framework
|
||||
prodBranch
|
||||
webhooks
|
||||
repository
|
||||
updatedAt
|
||||
icon
|
||||
subDomain
|
||||
deployments {
|
||||
id
|
||||
branch
|
||||
isCurrent
|
||||
status
|
||||
updatedAt
|
||||
commitHash
|
||||
commitMessage
|
||||
createdAt
|
||||
environment
|
||||
domain {
|
||||
status
|
||||
branch
|
||||
createdAt
|
||||
updatedAt
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getOrganizations = import_client.gql`
|
||||
query {
|
||||
organizations {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getDeployments = import_client.gql`
|
||||
query ($projectId: String!) {
|
||||
deployments(projectId: $projectId) {
|
||||
id
|
||||
domain{
|
||||
branch
|
||||
createdAt
|
||||
id
|
||||
name
|
||||
status
|
||||
updatedAt
|
||||
}
|
||||
branch
|
||||
commitHash
|
||||
commitMessage
|
||||
url
|
||||
environment
|
||||
isCurrent
|
||||
status
|
||||
createdAt
|
||||
updatedAt
|
||||
createdBy {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getEnvironmentVariables = import_client.gql`
|
||||
query ($projectId: String!) {
|
||||
environmentVariables(projectId: $projectId) {
|
||||
createdAt
|
||||
environment
|
||||
id
|
||||
key
|
||||
updatedAt
|
||||
value
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getProjectMembers = import_client.gql`
|
||||
query ($projectId: String!) {
|
||||
projectMembers(projectId: $projectId) {
|
||||
id
|
||||
member {
|
||||
id
|
||||
name
|
||||
email
|
||||
isVerified
|
||||
}
|
||||
isPending
|
||||
createdAt
|
||||
updatedAt
|
||||
permissions
|
||||
}
|
||||
}
|
||||
`;
|
||||
var searchProjects = import_client.gql`
|
||||
query ($searchText: String!) {
|
||||
searchProjects(searchText: $searchText) {
|
||||
id
|
||||
name
|
||||
prodBranch
|
||||
repository
|
||||
createdAt
|
||||
description
|
||||
framework
|
||||
prodBranch
|
||||
webhooks
|
||||
updatedAt
|
||||
template
|
||||
repository
|
||||
organization {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getDomains = import_client.gql`
|
||||
query ($projectId: String!, $filter: FilterDomainsInput) {
|
||||
domains(projectId: $projectId, filter: $filter) {
|
||||
branch
|
||||
createdAt
|
||||
redirectTo {
|
||||
id
|
||||
name
|
||||
branch
|
||||
status
|
||||
}
|
||||
id
|
||||
name
|
||||
status
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// src/mutations.ts
|
||||
var import_client2 = require("@apollo/client");
|
||||
var removeProjectMember = import_client2.gql`
|
||||
mutation ($projectMemberId: String!) {
|
||||
removeProjectMember(projectMemberId: $projectMemberId)
|
||||
}
|
||||
`;
|
||||
var updateProjectMember = import_client2.gql`
|
||||
mutation ($projectMemberId: String!, $data: UpdateProjectMemberInput) {
|
||||
updateProjectMember(projectMemberId: $projectMemberId, data: $data)
|
||||
}
|
||||
`;
|
||||
var addProjectMember = import_client2.gql`
|
||||
mutation ($projectId: String!, $data: AddProjectMemberInput) {
|
||||
addProjectMember(projectId: $projectId, data: $data)
|
||||
}
|
||||
`;
|
||||
var addEnvironmentVariables = import_client2.gql`
|
||||
mutation ($projectId: String!, $data: [AddEnvironmentVariableInput!]) {
|
||||
addEnvironmentVariables(projectId: $projectId, data: $data)
|
||||
}
|
||||
`;
|
||||
var updateEnvironmentVariable = import_client2.gql`
|
||||
mutation (
|
||||
$environmentVariableId: String!
|
||||
$data: UpdateEnvironmentVariableInput!
|
||||
) {
|
||||
updateEnvironmentVariable(
|
||||
environmentVariableId: $environmentVariableId
|
||||
data: $data
|
||||
)
|
||||
}
|
||||
`;
|
||||
var removeEnvironmentVariable = import_client2.gql`
|
||||
mutation ($environmentVariableId: String!) {
|
||||
removeEnvironmentVariable(environmentVariableId: $environmentVariableId)
|
||||
}
|
||||
`;
|
||||
var updateDeploymentToProd = import_client2.gql`
|
||||
mutation ($deploymentId: String!) {
|
||||
updateDeploymentToProd(deploymentId: $deploymentId)
|
||||
}
|
||||
`;
|
||||
var addProjectFromTemplate = import_client2.gql`
|
||||
mutation ($organizationSlug: String!, $data: AddProjectFromTemplateInput) {
|
||||
addProjectFromTemplate(organizationSlug: $organizationSlug, data: $data) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
var addProject = import_client2.gql`
|
||||
mutation ($organizationSlug: String!, $data: AddProjectInput) {
|
||||
addProject(organizationSlug: $organizationSlug, data: $data) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
var updateProjectMutation = import_client2.gql`
|
||||
mutation ($projectId: String!, $data: UpdateProjectInput) {
|
||||
updateProject(projectId: $projectId, data: $data)
|
||||
}
|
||||
`;
|
||||
var updateDomainMutation = import_client2.gql`
|
||||
mutation ($domainId: String!, $data: UpdateDomainInput!) {
|
||||
updateDomain(domainId: $domainId, data: $data)
|
||||
}
|
||||
`;
|
||||
var redeployToProd = import_client2.gql`
|
||||
mutation ($deploymentId: String!) {
|
||||
redeployToProd(deploymentId: $deploymentId)
|
||||
}
|
||||
`;
|
||||
var deleteProject = import_client2.gql`
|
||||
mutation ($projectId: String!) {
|
||||
deleteProject(projectId: $projectId)
|
||||
}
|
||||
`;
|
||||
var deleteDomain = import_client2.gql`
|
||||
mutation ($domainId: String!) {
|
||||
deleteDomain(domainId: $domainId)
|
||||
}
|
||||
`;
|
||||
var rollbackDeployment = import_client2.gql`
|
||||
mutation ($projectId: String!, $deploymentId: String!) {
|
||||
rollbackDeployment(projectId: $projectId, deploymentId: $deploymentId)
|
||||
}
|
||||
`;
|
||||
var deleteDeployment = import_client2.gql`
|
||||
mutation ($deploymentId: String!) {
|
||||
deleteDeployment(deploymentId: $deploymentId)
|
||||
}
|
||||
`;
|
||||
var addDomain = import_client2.gql`
|
||||
mutation ($projectId: String!, $data: AddDomainInput!) {
|
||||
addDomain(projectId: $projectId, data: $data)
|
||||
}
|
||||
`;
|
||||
var authenticateGitHub = import_client2.gql`
|
||||
mutation ($code: String!) {
|
||||
authenticateGitHub(code: $code) {
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
var unauthenticateGitHub = import_client2.gql`
|
||||
mutation {
|
||||
unauthenticateGitHub
|
||||
}
|
||||
`;
|
||||
|
||||
// src/client.ts
|
||||
var defaultOptions = {
|
||||
watchQuery: {
|
||||
fetchPolicy: "no-cache",
|
||||
errorPolicy: "ignore"
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: "no-cache",
|
||||
errorPolicy: "all"
|
||||
}
|
||||
};
|
||||
var GQLClient = class {
|
||||
constructor(config) {
|
||||
this.client = new import_client3.ApolloClient({
|
||||
uri: config.gqlEndpoint,
|
||||
cache: new import_client3.InMemoryCache(),
|
||||
defaultOptions,
|
||||
credentials: "include"
|
||||
});
|
||||
}
|
||||
getUser() {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getUser
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
getProject(projectId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getProject,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
getProjectsInOrganization(organizationSlug) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getProjectsInOrganization,
|
||||
variables: {
|
||||
organizationSlug
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
getOrganizations() {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getOrganizations
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
getDeployments(projectId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getDeployments,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
getEnvironmentVariables(projectId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getEnvironmentVariables,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
getProjectMembers(projectId) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.query({
|
||||
query: getProjectMembers,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
addProjectMember(projectId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: addProjectMember,
|
||||
variables: {
|
||||
projectId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
updateProjectMember(projectMemberId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: updateProjectMember,
|
||||
variables: {
|
||||
projectMemberId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
removeProjectMember(projectMemberId) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: removeProjectMember,
|
||||
variables: {
|
||||
projectMemberId
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
searchProjects(searchText) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: searchProjects,
|
||||
variables: {
|
||||
searchText
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
addEnvironmentVariables(projectId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: addEnvironmentVariables,
|
||||
variables: {
|
||||
projectId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
updateEnvironmentVariable(environmentVariableId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: updateEnvironmentVariable,
|
||||
variables: {
|
||||
environmentVariableId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
removeEnvironmentVariable(environmentVariableId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: removeEnvironmentVariable,
|
||||
variables: {
|
||||
environmentVariableId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
updateDeploymentToProd(deploymentId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: updateDeploymentToProd,
|
||||
variables: {
|
||||
deploymentId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
addProjectFromTemplate(organizationSlug, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: addProjectFromTemplate,
|
||||
variables: {
|
||||
organizationSlug,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
addProject(organizationSlug, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: addProject,
|
||||
variables: {
|
||||
organizationSlug,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
updateProject(projectId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: updateProjectMutation,
|
||||
variables: {
|
||||
projectId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
updateDomain(domainId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: updateDomainMutation,
|
||||
variables: {
|
||||
domainId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
redeployToProd(deploymentId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: redeployToProd,
|
||||
variables: {
|
||||
deploymentId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
deleteProject(projectId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: deleteProject,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
deleteDomain(domainId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: deleteDomain,
|
||||
variables: {
|
||||
domainId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
rollbackDeployment(projectId, deploymentId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: rollbackDeployment,
|
||||
variables: {
|
||||
projectId,
|
||||
deploymentId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
deleteDeployment(deploymentId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: deleteDeployment,
|
||||
variables: {
|
||||
deploymentId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
addDomain(projectId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: addDomain,
|
||||
variables: {
|
||||
projectId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
getDomains(projectId, filter) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getDomains,
|
||||
variables: {
|
||||
projectId,
|
||||
filter
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
authenticateGitHub(code) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: authenticateGitHub,
|
||||
variables: {
|
||||
code
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
unauthenticateGithub() {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: unauthenticateGitHub
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// src/types.ts
|
||||
var Role = /* @__PURE__ */ ((Role2) => {
|
||||
Role2["Owner"] = "Owner";
|
||||
Role2["Maintainer"] = "Maintainer";
|
||||
Role2["Reader"] = "Reader";
|
||||
return Role2;
|
||||
})(Role || {});
|
||||
var Permission = /* @__PURE__ */ ((Permission2) => {
|
||||
Permission2["View"] = "View";
|
||||
Permission2["Edit"] = "Edit";
|
||||
return Permission2;
|
||||
})(Permission || {});
|
||||
var Environment = /* @__PURE__ */ ((Environment2) => {
|
||||
Environment2["Production"] = "Production";
|
||||
Environment2["Preview"] = "Preview";
|
||||
Environment2["Development"] = "Development";
|
||||
return Environment2;
|
||||
})(Environment || {});
|
||||
var DeploymentStatus = /* @__PURE__ */ ((DeploymentStatus2) => {
|
||||
DeploymentStatus2["Building"] = "Building";
|
||||
DeploymentStatus2["Ready"] = "Ready";
|
||||
DeploymentStatus2["Error"] = "Error";
|
||||
DeploymentStatus2["Deleting"] = "Deleting";
|
||||
return DeploymentStatus2;
|
||||
})(DeploymentStatus || {});
|
||||
var DomainStatus = /* @__PURE__ */ ((DomainStatus2) => {
|
||||
DomainStatus2["Live"] = "Live";
|
||||
DomainStatus2["Pending"] = "Pending";
|
||||
return DomainStatus2;
|
||||
})(DomainStatus || {});
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
DeploymentStatus,
|
||||
DomainStatus,
|
||||
Environment,
|
||||
GQLClient,
|
||||
Permission,
|
||||
Role
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
1
packages/gql-client/dist/index.js.map
vendored
Normal file
1
packages/gql-client/dist/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
720
packages/gql-client/dist/index.mjs
vendored
Normal file
720
packages/gql-client/dist/index.mjs
vendored
Normal file
@ -0,0 +1,720 @@
|
||||
var __async = (__this, __arguments, generator) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
var fulfilled = (value) => {
|
||||
try {
|
||||
step(generator.next(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var rejected = (value) => {
|
||||
try {
|
||||
step(generator.throw(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
||||
step((generator = generator.apply(__this, __arguments)).next());
|
||||
});
|
||||
};
|
||||
|
||||
// src/client.ts
|
||||
import {
|
||||
ApolloClient,
|
||||
InMemoryCache
|
||||
} from "@apollo/client";
|
||||
|
||||
// src/queries.ts
|
||||
import { gql } from "@apollo/client";
|
||||
var getUser = gql`
|
||||
query {
|
||||
user {
|
||||
id
|
||||
name
|
||||
email
|
||||
createdAt
|
||||
updatedAt
|
||||
gitHubToken
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getProject = gql`
|
||||
query ($projectId: String!) {
|
||||
project(projectId: $projectId) {
|
||||
createdAt
|
||||
description
|
||||
id
|
||||
name
|
||||
template
|
||||
updatedAt
|
||||
prodBranch
|
||||
framework
|
||||
repository
|
||||
webhooks
|
||||
icon
|
||||
subDomain
|
||||
organization {
|
||||
id
|
||||
name
|
||||
}
|
||||
owner {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
deployments {
|
||||
id
|
||||
branch
|
||||
isCurrent
|
||||
status
|
||||
updatedAt
|
||||
commitHash
|
||||
createdAt
|
||||
environment
|
||||
domain {
|
||||
status
|
||||
branch
|
||||
createdAt
|
||||
updatedAt
|
||||
id
|
||||
name
|
||||
}
|
||||
createdBy {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getProjectsInOrganization = gql`
|
||||
query ($organizationSlug: String!) {
|
||||
projectsInOrganization(organizationSlug: $organizationSlug) {
|
||||
id
|
||||
name
|
||||
createdAt
|
||||
description
|
||||
framework
|
||||
prodBranch
|
||||
webhooks
|
||||
repository
|
||||
updatedAt
|
||||
icon
|
||||
subDomain
|
||||
deployments {
|
||||
id
|
||||
branch
|
||||
isCurrent
|
||||
status
|
||||
updatedAt
|
||||
commitHash
|
||||
commitMessage
|
||||
createdAt
|
||||
environment
|
||||
domain {
|
||||
status
|
||||
branch
|
||||
createdAt
|
||||
updatedAt
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getOrganizations = gql`
|
||||
query {
|
||||
organizations {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getDeployments = gql`
|
||||
query ($projectId: String!) {
|
||||
deployments(projectId: $projectId) {
|
||||
id
|
||||
domain{
|
||||
branch
|
||||
createdAt
|
||||
id
|
||||
name
|
||||
status
|
||||
updatedAt
|
||||
}
|
||||
branch
|
||||
commitHash
|
||||
commitMessage
|
||||
url
|
||||
environment
|
||||
isCurrent
|
||||
status
|
||||
createdAt
|
||||
updatedAt
|
||||
createdBy {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getEnvironmentVariables = gql`
|
||||
query ($projectId: String!) {
|
||||
environmentVariables(projectId: $projectId) {
|
||||
createdAt
|
||||
environment
|
||||
id
|
||||
key
|
||||
updatedAt
|
||||
value
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getProjectMembers = gql`
|
||||
query ($projectId: String!) {
|
||||
projectMembers(projectId: $projectId) {
|
||||
id
|
||||
member {
|
||||
id
|
||||
name
|
||||
email
|
||||
isVerified
|
||||
}
|
||||
isPending
|
||||
createdAt
|
||||
updatedAt
|
||||
permissions
|
||||
}
|
||||
}
|
||||
`;
|
||||
var searchProjects = gql`
|
||||
query ($searchText: String!) {
|
||||
searchProjects(searchText: $searchText) {
|
||||
id
|
||||
name
|
||||
prodBranch
|
||||
repository
|
||||
createdAt
|
||||
description
|
||||
framework
|
||||
prodBranch
|
||||
webhooks
|
||||
updatedAt
|
||||
template
|
||||
repository
|
||||
organization {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
var getDomains = gql`
|
||||
query ($projectId: String!, $filter: FilterDomainsInput) {
|
||||
domains(projectId: $projectId, filter: $filter) {
|
||||
branch
|
||||
createdAt
|
||||
redirectTo {
|
||||
id
|
||||
name
|
||||
branch
|
||||
status
|
||||
}
|
||||
id
|
||||
name
|
||||
status
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// src/mutations.ts
|
||||
import { gql as gql2 } from "@apollo/client";
|
||||
var removeProjectMember = gql2`
|
||||
mutation ($projectMemberId: String!) {
|
||||
removeProjectMember(projectMemberId: $projectMemberId)
|
||||
}
|
||||
`;
|
||||
var updateProjectMember = gql2`
|
||||
mutation ($projectMemberId: String!, $data: UpdateProjectMemberInput) {
|
||||
updateProjectMember(projectMemberId: $projectMemberId, data: $data)
|
||||
}
|
||||
`;
|
||||
var addProjectMember = gql2`
|
||||
mutation ($projectId: String!, $data: AddProjectMemberInput) {
|
||||
addProjectMember(projectId: $projectId, data: $data)
|
||||
}
|
||||
`;
|
||||
var addEnvironmentVariables = gql2`
|
||||
mutation ($projectId: String!, $data: [AddEnvironmentVariableInput!]) {
|
||||
addEnvironmentVariables(projectId: $projectId, data: $data)
|
||||
}
|
||||
`;
|
||||
var updateEnvironmentVariable = gql2`
|
||||
mutation (
|
||||
$environmentVariableId: String!
|
||||
$data: UpdateEnvironmentVariableInput!
|
||||
) {
|
||||
updateEnvironmentVariable(
|
||||
environmentVariableId: $environmentVariableId
|
||||
data: $data
|
||||
)
|
||||
}
|
||||
`;
|
||||
var removeEnvironmentVariable = gql2`
|
||||
mutation ($environmentVariableId: String!) {
|
||||
removeEnvironmentVariable(environmentVariableId: $environmentVariableId)
|
||||
}
|
||||
`;
|
||||
var updateDeploymentToProd = gql2`
|
||||
mutation ($deploymentId: String!) {
|
||||
updateDeploymentToProd(deploymentId: $deploymentId)
|
||||
}
|
||||
`;
|
||||
var addProjectFromTemplate = gql2`
|
||||
mutation ($organizationSlug: String!, $data: AddProjectFromTemplateInput) {
|
||||
addProjectFromTemplate(organizationSlug: $organizationSlug, data: $data) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
var addProject = gql2`
|
||||
mutation ($organizationSlug: String!, $data: AddProjectInput) {
|
||||
addProject(organizationSlug: $organizationSlug, data: $data) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
var updateProjectMutation = gql2`
|
||||
mutation ($projectId: String!, $data: UpdateProjectInput) {
|
||||
updateProject(projectId: $projectId, data: $data)
|
||||
}
|
||||
`;
|
||||
var updateDomainMutation = gql2`
|
||||
mutation ($domainId: String!, $data: UpdateDomainInput!) {
|
||||
updateDomain(domainId: $domainId, data: $data)
|
||||
}
|
||||
`;
|
||||
var redeployToProd = gql2`
|
||||
mutation ($deploymentId: String!) {
|
||||
redeployToProd(deploymentId: $deploymentId)
|
||||
}
|
||||
`;
|
||||
var deleteProject = gql2`
|
||||
mutation ($projectId: String!) {
|
||||
deleteProject(projectId: $projectId)
|
||||
}
|
||||
`;
|
||||
var deleteDomain = gql2`
|
||||
mutation ($domainId: String!) {
|
||||
deleteDomain(domainId: $domainId)
|
||||
}
|
||||
`;
|
||||
var rollbackDeployment = gql2`
|
||||
mutation ($projectId: String!, $deploymentId: String!) {
|
||||
rollbackDeployment(projectId: $projectId, deploymentId: $deploymentId)
|
||||
}
|
||||
`;
|
||||
var deleteDeployment = gql2`
|
||||
mutation ($deploymentId: String!) {
|
||||
deleteDeployment(deploymentId: $deploymentId)
|
||||
}
|
||||
`;
|
||||
var addDomain = gql2`
|
||||
mutation ($projectId: String!, $data: AddDomainInput!) {
|
||||
addDomain(projectId: $projectId, data: $data)
|
||||
}
|
||||
`;
|
||||
var authenticateGitHub = gql2`
|
||||
mutation ($code: String!) {
|
||||
authenticateGitHub(code: $code) {
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
var unauthenticateGitHub = gql2`
|
||||
mutation {
|
||||
unauthenticateGitHub
|
||||
}
|
||||
`;
|
||||
|
||||
// src/client.ts
|
||||
var defaultOptions = {
|
||||
watchQuery: {
|
||||
fetchPolicy: "no-cache",
|
||||
errorPolicy: "ignore"
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: "no-cache",
|
||||
errorPolicy: "all"
|
||||
}
|
||||
};
|
||||
var GQLClient = class {
|
||||
constructor(config) {
|
||||
this.client = new ApolloClient({
|
||||
uri: config.gqlEndpoint,
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions,
|
||||
credentials: "include"
|
||||
});
|
||||
}
|
||||
getUser() {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getUser
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
getProject(projectId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getProject,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
getProjectsInOrganization(organizationSlug) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getProjectsInOrganization,
|
||||
variables: {
|
||||
organizationSlug
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
getOrganizations() {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getOrganizations
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
getDeployments(projectId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getDeployments,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
getEnvironmentVariables(projectId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getEnvironmentVariables,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
getProjectMembers(projectId) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.query({
|
||||
query: getProjectMembers,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
addProjectMember(projectId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: addProjectMember,
|
||||
variables: {
|
||||
projectId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
updateProjectMember(projectMemberId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: updateProjectMember,
|
||||
variables: {
|
||||
projectMemberId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
removeProjectMember(projectMemberId) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: removeProjectMember,
|
||||
variables: {
|
||||
projectMemberId
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
searchProjects(searchText) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: searchProjects,
|
||||
variables: {
|
||||
searchText
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
addEnvironmentVariables(projectId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: addEnvironmentVariables,
|
||||
variables: {
|
||||
projectId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
updateEnvironmentVariable(environmentVariableId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: updateEnvironmentVariable,
|
||||
variables: {
|
||||
environmentVariableId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
removeEnvironmentVariable(environmentVariableId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: removeEnvironmentVariable,
|
||||
variables: {
|
||||
environmentVariableId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
updateDeploymentToProd(deploymentId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: updateDeploymentToProd,
|
||||
variables: {
|
||||
deploymentId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
addProjectFromTemplate(organizationSlug, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: addProjectFromTemplate,
|
||||
variables: {
|
||||
organizationSlug,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
addProject(organizationSlug, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: addProject,
|
||||
variables: {
|
||||
organizationSlug,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
updateProject(projectId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: updateProjectMutation,
|
||||
variables: {
|
||||
projectId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
updateDomain(domainId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: updateDomainMutation,
|
||||
variables: {
|
||||
domainId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
redeployToProd(deploymentId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: redeployToProd,
|
||||
variables: {
|
||||
deploymentId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
deleteProject(projectId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: deleteProject,
|
||||
variables: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
deleteDomain(domainId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: deleteDomain,
|
||||
variables: {
|
||||
domainId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
rollbackDeployment(projectId, deploymentId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: rollbackDeployment,
|
||||
variables: {
|
||||
projectId,
|
||||
deploymentId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
deleteDeployment(deploymentId) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: deleteDeployment,
|
||||
variables: {
|
||||
deploymentId
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
addDomain(projectId, data) {
|
||||
return __async(this, null, function* () {
|
||||
const result = yield this.client.mutate({
|
||||
mutation: addDomain,
|
||||
variables: {
|
||||
projectId,
|
||||
data
|
||||
}
|
||||
});
|
||||
return result.data;
|
||||
});
|
||||
}
|
||||
getDomains(projectId, filter) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.query({
|
||||
query: getDomains,
|
||||
variables: {
|
||||
projectId,
|
||||
filter
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
authenticateGitHub(code) {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: authenticateGitHub,
|
||||
variables: {
|
||||
code
|
||||
}
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
unauthenticateGithub() {
|
||||
return __async(this, null, function* () {
|
||||
const { data } = yield this.client.mutate({
|
||||
mutation: unauthenticateGitHub
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// src/types.ts
|
||||
var Role = /* @__PURE__ */ ((Role2) => {
|
||||
Role2["Owner"] = "Owner";
|
||||
Role2["Maintainer"] = "Maintainer";
|
||||
Role2["Reader"] = "Reader";
|
||||
return Role2;
|
||||
})(Role || {});
|
||||
var Permission = /* @__PURE__ */ ((Permission2) => {
|
||||
Permission2["View"] = "View";
|
||||
Permission2["Edit"] = "Edit";
|
||||
return Permission2;
|
||||
})(Permission || {});
|
||||
var Environment = /* @__PURE__ */ ((Environment2) => {
|
||||
Environment2["Production"] = "Production";
|
||||
Environment2["Preview"] = "Preview";
|
||||
Environment2["Development"] = "Development";
|
||||
return Environment2;
|
||||
})(Environment || {});
|
||||
var DeploymentStatus = /* @__PURE__ */ ((DeploymentStatus2) => {
|
||||
DeploymentStatus2["Building"] = "Building";
|
||||
DeploymentStatus2["Ready"] = "Ready";
|
||||
DeploymentStatus2["Error"] = "Error";
|
||||
DeploymentStatus2["Deleting"] = "Deleting";
|
||||
return DeploymentStatus2;
|
||||
})(DeploymentStatus || {});
|
||||
var DomainStatus = /* @__PURE__ */ ((DomainStatus2) => {
|
||||
DomainStatus2["Live"] = "Live";
|
||||
DomainStatus2["Pending"] = "Pending";
|
||||
return DomainStatus2;
|
||||
})(DomainStatus || {});
|
||||
export {
|
||||
DeploymentStatus,
|
||||
DomainStatus,
|
||||
Environment,
|
||||
GQLClient,
|
||||
Permission,
|
||||
Role
|
||||
};
|
||||
//# sourceMappingURL=index.mjs.map
|
1
packages/gql-client/dist/index.mjs.map
vendored
Normal file
1
packages/gql-client/dist/index.mjs.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -4,7 +4,6 @@
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./src",
|
||||
"scripts": {
|
||||
"build": "npx tsup src/index.ts --dts --format esm,cjs --sourcemap",
|
||||
"lint": "tsc --noEmit"
|
||||
@ -17,4 +16,4 @@
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.8.9"
|
||||
}
|
||||
}
|
||||
}
|
@ -230,19 +230,13 @@ export class GQLClient {
|
||||
|
||||
async addProjectFromTemplate(
|
||||
organizationSlug: string,
|
||||
data: types.AddProjectFromTemplateInput,
|
||||
lrn?: string,
|
||||
auctionParams?: types.AuctionParams,
|
||||
environmentVariables?: types.AddEnvironmentVariableInput[]
|
||||
data: types.AddProjectFromTemplateInput
|
||||
): Promise<types.AddProjectFromTemplateResponse> {
|
||||
const result = await this.client.mutate({
|
||||
mutation: mutations.addProjectFromTemplate,
|
||||
variables: {
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
},
|
||||
});
|
||||
|
||||
@ -251,19 +245,13 @@ export class GQLClient {
|
||||
|
||||
async addProject(
|
||||
organizationSlug: string,
|
||||
data: types.AddProjectInput,
|
||||
lrn?: string,
|
||||
auctionParams?: types.AuctionParams,
|
||||
environmentVariables?: types.AddEnvironmentVariableInput[]
|
||||
data: types.AddProjectInput
|
||||
): Promise<types.AddProjectResponse> {
|
||||
const result = await this.client.mutate({
|
||||
mutation: mutations.addProject,
|
||||
variables: {
|
||||
organizationSlug,
|
||||
data,
|
||||
lrn,
|
||||
auctionParams,
|
||||
environmentVariables
|
||||
},
|
||||
});
|
||||
|
||||
@ -413,44 +401,4 @@ export class GQLClient {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getAuctionData(auctionId: string): Promise<types.Auction> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getAuctionData,
|
||||
variables: {
|
||||
auctionId,
|
||||
},
|
||||
});
|
||||
|
||||
return data.getAuctionData;
|
||||
}
|
||||
|
||||
async getDeployers(): Promise<types.GetDeployersResponse> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getDeployers,
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async getAddress(): Promise<string> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.getAddress,
|
||||
});
|
||||
|
||||
return data.address;
|
||||
}
|
||||
|
||||
async verifyTx(txHash: string, amount: string, senderAddress: string): Promise<boolean> {
|
||||
const { data } = await this.client.query({
|
||||
query: queries.verifyTx,
|
||||
variables: {
|
||||
txHash,
|
||||
amount,
|
||||
senderAddress
|
||||
}
|
||||
});
|
||||
|
||||
return data.verifyTx;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user