forked from cerc-io/snowballtools-base
Merge branch 'ng-check-deployment-removal-record'
This commit is contained in:
commit
6dfe85cb1a
34
README.md
34
README.md
@ -99,23 +99,11 @@ Let us assume the following domains for backend and frontend
|
|||||||
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
|
- Get the private key and set `registryConfig.privateKey` in backend [config file](packages/backend/environments/local.toml)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
laconic-so --stack fixturenet-laconic-loaded deploy exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
|
laconic-so deployment --dir laconic-loaded-deployment exec laconicd "laconicd keys export mykey --unarmored-hex --unsafe"
|
||||||
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
|
# WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue? [y/N]: y
|
||||||
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
|
# 754cca7b4b729a99d156913aea95366411d072856666e95ba09ef6c664357d81
|
||||||
```
|
```
|
||||||
|
|
||||||
- Get the REST and GQL endpoint ports of Laconicd and replace the ports for `registryConfig.restEndpoint` and `registryConfig.gqlEndpoint` in backend [config file](packages/backend/environments/local.toml)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For registryConfig.restEndpoint
|
|
||||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 1317
|
|
||||||
# 0.0.0.0:32777
|
|
||||||
|
|
||||||
# For registryConfig.gqlEndpoint
|
|
||||||
laconic-so --stack fixturenet-laconic-loaded deploy port laconicd 9473
|
|
||||||
# 0.0.0.0:32771
|
|
||||||
```
|
|
||||||
|
|
||||||
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
|
- Set authority in `registryConfig.authority` in backend [config file](packages/backend/environments/local.toml)
|
||||||
|
|
||||||
- Run the script to create bond, reserve the authority and set authority bond
|
- Run the script to create bond, reserve the authority and set authority bond
|
||||||
@ -157,14 +145,14 @@ Let us assume the following domains for backend and frontend
|
|||||||
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
|
- Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
|
||||||
|
|
||||||
```env
|
```env
|
||||||
REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
|
VITE_GITHUB_CLIENT_ID = <CLIENT_ID>
|
||||||
```
|
```
|
||||||
|
|
||||||
- Set `REACT_APP_GITHUB_PWA_TEMPLATE_REPO` and `REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
- Set `VITE_GITHUB_PWA_TEMPLATE_REPO` and `VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO` in [.env](packages/frontend/.env) file
|
||||||
|
|
||||||
```env
|
```env
|
||||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = 'cerc-io/test-progressive-web-app' # Set actual owner/name of the template repo that will be used for creating new repo
|
VITE_GITHUB_PWA_TEMPLATE_REPO = 'cerc-io/test-progressive-web-app' # Set actual owner/name of the template repo that will be used for creating new repo
|
||||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'cerc-io/image-upload-pwa-example' # Set actual owner/name of the template repo that will be used for creating new repo
|
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'cerc-io/image-upload-pwa-example' # Set actual owner/name of the template repo that will be used for creating new repo
|
||||||
```
|
```
|
||||||
|
|
||||||
- Production
|
- Production
|
||||||
@ -172,17 +160,17 @@ Let us assume the following domains for backend and frontend
|
|||||||
- Set the following values in [.env](packages/frontend/.env) file
|
- Set the following values in [.env](packages/frontend/.env) file
|
||||||
|
|
||||||
```env
|
```env
|
||||||
REACT_APP_SERVER_URL = 'https://api.snowballtools.com' # Backend server endpoint
|
VITE_SERVER_URL = 'https://api.snowballtools.com' # Backend server endpoint
|
||||||
```
|
```
|
||||||
|
|
||||||
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
|
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID
|
||||||
- Create a project and add information to use wallet connect SDK
|
- Create a project and add information to use wallet connect SDK
|
||||||
- Add project name and select project type as `App`
|
- Add project name and select project type as `App`
|
||||||
- Set project home page URL to `https://dashboard.snowballtools.com`
|
- Set project home page URL to `https://dashboard.snowballtools.com`
|
||||||
- On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
- On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
||||||
|
|
||||||
```env
|
```env
|
||||||
REACT_APP_WALLET_CONNECT_ID = <PROJECT_ID>
|
VITE_WALLET_CONNECT_ID = <PROJECT_ID>
|
||||||
```
|
```
|
||||||
|
|
||||||
- Build the React application
|
- Build the React application
|
||||||
@ -202,17 +190,17 @@ Let us assume the following domains for backend and frontend
|
|||||||
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
|
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
|
||||||
|
|
||||||
```env
|
```env
|
||||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
VITE_SERVER_URL = 'http://localhost:8000'
|
||||||
```
|
```
|
||||||
|
|
||||||
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID.
|
- Sign in to [wallet connect](https://cloud.walletconnect.com/sign-in) to create a project ID.
|
||||||
- Create a project and add information to use wallet connect SDK
|
- Create a project and add information to use wallet connect SDK
|
||||||
- Add project name and select project type as `App`
|
- Add project name and select project type as `App`
|
||||||
- Project home page URL is not required to be set
|
- Project home page URL is not required to be set
|
||||||
- On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
- On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file
|
||||||
|
|
||||||
```env
|
```env
|
||||||
REACT_APP_WALLET_CONNECT_ID = <Project_ID>
|
VITE_WALLET_CONNECT_ID = <Project_ID>
|
||||||
```
|
```
|
||||||
|
|
||||||
- The React application will be running in `http://localhost:3000/`
|
- The React application will be running in `http://localhost:3000/`
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
"lint": "tsc --noEmit",
|
"lint": "tsc --noEmit",
|
||||||
"test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
|
"test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
|
||||||
"test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts",
|
"test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts",
|
||||||
|
"test:registry:publish-deployment-removal-records": "DEBUG=snowball:* ts-node ./test/publish-deployment-removal-records.ts",
|
||||||
"test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
|
"test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
|
||||||
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
|
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
|
||||||
},
|
},
|
||||||
|
@ -436,6 +436,19 @@ export class Database {
|
|||||||
return Boolean(updateResult.affected);
|
return Boolean(updateResult.affected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteDeploymentById (deploymentId: string): Promise<boolean> {
|
||||||
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
|
const deployment = await deploymentRepository.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
id: deploymentId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteResult = await deploymentRepository.softRemove(deployment);
|
||||||
|
|
||||||
|
return Boolean(deleteResult);
|
||||||
|
}
|
||||||
|
|
||||||
async addProject (user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
|
async addProject (user: User, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
|
@ -6,13 +6,14 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
JoinColumn
|
JoinColumn,
|
||||||
|
DeleteDateColumn
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { Project } from './Project';
|
import { Project } from './Project';
|
||||||
import { Domain } from './Domain';
|
import { Domain } from './Domain';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
import { AppDeploymentRecordAttributes } from '../types';
|
import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types';
|
||||||
|
|
||||||
export enum Environment {
|
export enum Environment {
|
||||||
Production = 'Production',
|
Production = 'Production',
|
||||||
@ -24,6 +25,7 @@ export enum DeploymentStatus {
|
|||||||
Building = 'Building',
|
Building = 'Building',
|
||||||
Ready = 'Ready',
|
Ready = 'Ready',
|
||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
|
Deleting = 'Deleting',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApplicationDeploymentRequest {
|
export interface ApplicationDeploymentRequest {
|
||||||
@ -35,6 +37,18 @@ export interface ApplicationDeploymentRequest {
|
|||||||
meta: string;
|
meta: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApplicationDeploymentRemovalRequest {
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
deployment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationDeploymentRemovalRequest {
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
deployment: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApplicationRecord {
|
export interface ApplicationRecord {
|
||||||
type: string;
|
type: string;
|
||||||
version: string;
|
version: string;
|
||||||
@ -99,6 +113,18 @@ export class Deployment {
|
|||||||
@Column('simple-json', { nullable: true })
|
@Column('simple-json', { nullable: true })
|
||||||
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
|
applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
applicationDeploymentRemovalRequestId!: string | null;
|
||||||
|
|
||||||
|
@Column('simple-json', { nullable: true })
|
||||||
|
applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true })
|
||||||
|
applicationDeploymentRemovalRecordId!: string | null;
|
||||||
|
|
||||||
|
@Column('simple-json', { nullable: true })
|
||||||
|
applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
enum: Environment
|
enum: Environment
|
||||||
})
|
})
|
||||||
@ -121,4 +147,7 @@ export class Deployment {
|
|||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt!: Date | null;
|
||||||
}
|
}
|
||||||
|
@ -9,16 +9,19 @@ import { RegistryConfig } from './config';
|
|||||||
import {
|
import {
|
||||||
ApplicationRecord,
|
ApplicationRecord,
|
||||||
Deployment,
|
Deployment,
|
||||||
ApplicationDeploymentRequest
|
ApplicationDeploymentRequest,
|
||||||
|
ApplicationDeploymentRemovalRequest
|
||||||
} from './entity/Deployment';
|
} from './entity/Deployment';
|
||||||
import { AppDeploymentRecord, PackageJSON } from './types';
|
import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types';
|
||||||
import { sleep } from './utils';
|
import { sleep } from './utils';
|
||||||
|
|
||||||
const log = debug('snowball:registry');
|
const log = debug('snowball:registry');
|
||||||
|
|
||||||
const APP_RECORD_TYPE = 'ApplicationRecord';
|
const APP_RECORD_TYPE = 'ApplicationRecord';
|
||||||
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest';
|
||||||
|
const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest';
|
||||||
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord';
|
||||||
|
const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord';
|
||||||
const SLEEP_DURATION = 1000;
|
const SLEEP_DURATION = 1000;
|
||||||
|
|
||||||
// TODO: Move registry code to laconic-sdk/watcher-ts
|
// TODO: Move registry code to laconic-sdk/watcher-ts
|
||||||
@ -229,6 +232,74 @@ export class Registry {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch ApplicationDeploymentRecords by filter
|
||||||
|
*/
|
||||||
|
async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise<AppDeploymentRecord[]> {
|
||||||
|
return this.registry.queryRecords(
|
||||||
|
{
|
||||||
|
type: APP_DEPLOYMENT_RECORD_TYPE,
|
||||||
|
...filter
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch ApplicationDeploymentRemovalRecords for deployments
|
||||||
|
*/
|
||||||
|
async getDeploymentRemovalRecords (
|
||||||
|
deployments: Deployment[]
|
||||||
|
): Promise<AppDeploymentRemovalRecord[]> {
|
||||||
|
// Fetch ApplicationDeploymentRemovalRecords for corresponding ApplicationDeploymentRecord set in deployments
|
||||||
|
const records = await this.registry.queryRecords(
|
||||||
|
{
|
||||||
|
type: APP_DEPLOYMENT_REMOVAL_RECORD_TYPE
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter records with ApplicationDeploymentRecord and ApplicationDeploymentRemovalRequest IDs
|
||||||
|
return records.filter((record: AppDeploymentRemovalRecord) =>
|
||||||
|
deployments.some(
|
||||||
|
(deployment) =>
|
||||||
|
deployment.applicationDeploymentRemovalRequestId === record.attributes.request &&
|
||||||
|
deployment.applicationDeploymentRecordId === record.attributes.deployment
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createApplicationDeploymentRemovalRequest (data: {
|
||||||
|
deploymentId: string;
|
||||||
|
}): Promise<{
|
||||||
|
applicationDeploymentRemovalRequestId: string;
|
||||||
|
applicationDeploymentRemovalRequestData: ApplicationDeploymentRemovalRequest;
|
||||||
|
}> {
|
||||||
|
const applicationDeploymentRemovalRequest = {
|
||||||
|
type: APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE,
|
||||||
|
version: '1.0.0',
|
||||||
|
deployment: data.deploymentId
|
||||||
|
};
|
||||||
|
|
||||||
|
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.data.id}`);
|
||||||
|
log('Application deployment removal request data:', applicationDeploymentRemovalRequest);
|
||||||
|
|
||||||
|
return {
|
||||||
|
applicationDeploymentRemovalRequestId: result.data.id,
|
||||||
|
applicationDeploymentRemovalRequestData: applicationDeploymentRemovalRequest
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
getCrn (appName: string): string {
|
getCrn (appName: string): string {
|
||||||
assert(this.registryConfig.authority, "Authority doesn't exist");
|
assert(this.registryConfig.authority, "Authority doesn't exist");
|
||||||
return `crn://${this.registryConfig.authority}/applications/${appName}`;
|
return `crn://${this.registryConfig.authority}/applications/${appName}`;
|
||||||
|
@ -255,6 +255,20 @@ export const createResolvers = async (service: Service): Promise<any> => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteDeployment: async (
|
||||||
|
_: any,
|
||||||
|
{
|
||||||
|
deploymentId
|
||||||
|
}: { deploymentId: string; }
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await service.deleteDeployment(deploymentId);
|
||||||
|
} catch (err) {
|
||||||
|
log(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
addDomain: async (
|
addDomain: async (
|
||||||
_: any,
|
_: any,
|
||||||
{ projectId, data }: { projectId: string; data: { name: string } }
|
{ projectId, data }: { projectId: string; data: { name: string } }
|
||||||
|
@ -19,6 +19,7 @@ enum DeploymentStatus {
|
|||||||
Building
|
Building
|
||||||
Ready
|
Ready
|
||||||
Error
|
Error
|
||||||
|
Deleting
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DomainStatus {
|
enum DomainStatus {
|
||||||
@ -209,6 +210,7 @@ type Mutation {
|
|||||||
deleteProject(projectId: String!): Boolean!
|
deleteProject(projectId: String!): Boolean!
|
||||||
deleteDomain(domainId: String!): Boolean!
|
deleteDomain(domainId: String!): Boolean!
|
||||||
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
|
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
|
||||||
|
deleteDeployment(deploymentId: String!): Boolean!
|
||||||
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
|
addDomain(projectId: String!, data: AddDomainInput!): Boolean!
|
||||||
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
|
updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
|
||||||
authenticateGitHub(code: String!): AuthResult!
|
authenticateGitHub(code: String!): AuthResult!
|
||||||
|
@ -15,13 +15,16 @@ import { Permission, ProjectMember } from './entity/ProjectMember';
|
|||||||
import { User } from './entity/User';
|
import { User } from './entity/User';
|
||||||
import { Registry } from './registry';
|
import { Registry } from './registry';
|
||||||
import { GitHubConfig, RegistryConfig } from './config';
|
import { GitHubConfig, RegistryConfig } from './config';
|
||||||
import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types';
|
import { AppDeploymentRecord, AppDeploymentRemovalRecord, GitPushEventPayload, PackageJSON } from './types';
|
||||||
import { Role } from './entity/UserOrganization';
|
import { Role } from './entity/UserOrganization';
|
||||||
|
|
||||||
const log = debug('snowball:service');
|
const log = debug('snowball:service');
|
||||||
|
|
||||||
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
|
const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository';
|
||||||
|
|
||||||
|
// Define a constant for an hour in milliseconds
|
||||||
|
const HOUR = 1000 * 60 * 60;
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
gitHubConfig: GitHubConfig;
|
gitHubConfig: GitHubConfig;
|
||||||
registryConfig: RegistryConfig;
|
registryConfig: RegistryConfig;
|
||||||
@ -49,6 +52,8 @@ export class Service {
|
|||||||
init (): void {
|
init (): void {
|
||||||
// Start check for ApplicationDeploymentRecords asynchronously
|
// Start check for ApplicationDeploymentRecords asynchronously
|
||||||
this.checkDeployRecordsAndUpdate();
|
this.checkDeployRecordsAndUpdate();
|
||||||
|
// Start check for ApplicationDeploymentRemovalRecords asynchronously
|
||||||
|
this.checkDeploymentRemovalRecordsAndUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,14 +65,13 @@ export class Service {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for ApplicationDeploymentRecord and update corresponding deployments
|
* Checks for ApplicationDeploymentRecord and update corresponding deployments
|
||||||
* Continues check in loop after a delay of DEPLOY_RECORD_CHECK_DELAY_MS
|
* Continues check in loop after a delay of registryConfig.fetchDeploymentRecordDelay
|
||||||
*/
|
*/
|
||||||
async checkDeployRecordsAndUpdate (): Promise<void> {
|
async checkDeployRecordsAndUpdate (): Promise<void> {
|
||||||
// Fetch deployments in building state
|
// Fetch deployments in building state
|
||||||
const deployments = await this.db.getDeployments({
|
const deployments = await this.db.getDeployments({
|
||||||
where: {
|
where: {
|
||||||
status: DeploymentStatus.Building
|
status: DeploymentStatus.Building
|
||||||
// TODO: Fetch and check records for recent deployments
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -76,6 +80,28 @@ export class Service {
|
|||||||
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`
|
`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Calculate a timestamp for one hour ago
|
||||||
|
const anHourAgo = Date.now() - HOUR;
|
||||||
|
|
||||||
|
// Filter out deployments started more than an hour ago and mark them as Error
|
||||||
|
const oldDeploymentsToUpdate = deployments.filter(
|
||||||
|
deployment => (Number(deployment.updatedAt) < anHourAgo)
|
||||||
|
)
|
||||||
|
.map((deployment) => {
|
||||||
|
return this.db.updateDeploymentById(deployment.id, {
|
||||||
|
status: DeploymentStatus.Error,
|
||||||
|
isCurrent: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there are old deployments to update, log and perform the updates
|
||||||
|
if (oldDeploymentsToUpdate.length > 0) {
|
||||||
|
log(
|
||||||
|
`Cleaning up ${oldDeploymentsToUpdate.length} deployments stuck in ${DeploymentStatus.Building} state for over an hour`
|
||||||
|
);
|
||||||
|
await Promise.all(oldDeploymentsToUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch ApplicationDeploymentRecord for deployments
|
// Fetch ApplicationDeploymentRecord for deployments
|
||||||
const records = await this.registry.getDeploymentRecords(deployments);
|
const records = await this.registry.getDeploymentRecords(deployments);
|
||||||
log(`Found ${records.length} ApplicationDeploymentRecords`);
|
log(`Found ${records.length} ApplicationDeploymentRecords`);
|
||||||
@ -91,6 +117,38 @@ export class Service {
|
|||||||
}, this.config.registryConfig.fetchDeploymentRecordDelay);
|
}, this.config.registryConfig.fetchDeploymentRecordDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for ApplicationDeploymentRemovalRecord and remove corresponding deployments
|
||||||
|
* Continues check in loop after a delay of registryConfig.fetchDeploymentRecordDelay
|
||||||
|
*/
|
||||||
|
async checkDeploymentRemovalRecordsAndUpdate (): Promise<void> {
|
||||||
|
// Fetch deployments in deleting state
|
||||||
|
const deployments = await this.db.getDeployments({
|
||||||
|
where: {
|
||||||
|
status: DeploymentStatus.Deleting
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deployments.length) {
|
||||||
|
log(
|
||||||
|
`Found ${deployments.length} deployments in ${DeploymentStatus.Deleting} state`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch ApplicationDeploymentRemovalRecords for deployments
|
||||||
|
const records = await this.registry.getDeploymentRemovalRecords(deployments);
|
||||||
|
log(`Found ${records.length} ApplicationDeploymentRemovalRecords`);
|
||||||
|
|
||||||
|
// Update deployments for which ApplicationDeploymentRemovalRecords were returned
|
||||||
|
if (records.length) {
|
||||||
|
await this.deleteDeploymentsWithRecordData(records, deployments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deployRecordCheckTimeout = setTimeout(() => {
|
||||||
|
this.checkDeploymentRemovalRecordsAndUpdate();
|
||||||
|
}, this.config.registryConfig.fetchDeploymentRecordDelay);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update deployments with ApplicationDeploymentRecord data
|
* Update deployments with ApplicationDeploymentRecord data
|
||||||
*/
|
*/
|
||||||
@ -153,6 +211,45 @@ export class Service {
|
|||||||
await Promise.all(deploymentUpdatePromises);
|
await Promise.all(deploymentUpdatePromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete deployments with ApplicationDeploymentRemovalRecord data
|
||||||
|
*/
|
||||||
|
async deleteDeploymentsWithRecordData (
|
||||||
|
records: AppDeploymentRemovalRecord[],
|
||||||
|
deployments: Deployment[],
|
||||||
|
): Promise<void> {
|
||||||
|
const removedApplicationDeploymentRecordIds = records.map(record => record.attributes.deployment);
|
||||||
|
|
||||||
|
// Get removed deployments for ApplicationDeploymentRecords
|
||||||
|
const removedDeployments = deployments.filter(deployment => removedApplicationDeploymentRecordIds.includes(deployment.applicationDeploymentRecordId!))
|
||||||
|
|
||||||
|
const recordToDeploymentsMap = removedDeployments.reduce(
|
||||||
|
(acc: { [key: string]: Deployment }, deployment) => {
|
||||||
|
acc[deployment.applicationDeploymentRecordId!] = deployment;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update deployment data for ApplicationDeploymentRecords and delete
|
||||||
|
const deploymentUpdatePromises = records.map(async (record) => {
|
||||||
|
const deployment = recordToDeploymentsMap[record.attributes.deployment];
|
||||||
|
|
||||||
|
await this.db.updateDeploymentById(deployment.id, {
|
||||||
|
applicationDeploymentRemovalRecordId: record.id,
|
||||||
|
applicationDeploymentRemovalRecordData: record.attributes,
|
||||||
|
});
|
||||||
|
|
||||||
|
log(
|
||||||
|
`Updated deployment ${deployment.id} with ApplicationDeploymentRemovalRecord ${record.id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.db.deleteDeploymentById(deployment.id)
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(deploymentUpdatePromises);
|
||||||
|
}
|
||||||
|
|
||||||
async getUser (userId: string): Promise<User | null> {
|
async getUser (userId: string): Promise<User | null> {
|
||||||
return this.db.getUser({
|
return this.db.getUser({
|
||||||
where: {
|
where: {
|
||||||
@ -479,17 +576,10 @@ export class Service {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as { [key: string]: string });
|
}, {} as { [key: string]: string });
|
||||||
|
|
||||||
const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest(
|
|
||||||
{
|
|
||||||
deployment: newDeployment,
|
|
||||||
appName: repo,
|
|
||||||
repository: repoUrl,
|
|
||||||
environmentVariables: environmentVariablesObj,
|
|
||||||
dns: `${newDeployment.project.name}-${newDeployment.id}`
|
|
||||||
});
|
|
||||||
|
|
||||||
// To set project DNS
|
// To set project DNS
|
||||||
if (data.environment === Environment.Production) {
|
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(
|
await this.registry.createApplicationDeploymentRequest(
|
||||||
{
|
{
|
||||||
deployment: newDeployment,
|
deployment: newDeployment,
|
||||||
@ -500,6 +590,15 @@ export class Service {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest(
|
||||||
|
{
|
||||||
|
deployment: newDeployment,
|
||||||
|
appName: repo,
|
||||||
|
repository: repoUrl,
|
||||||
|
environmentVariables: environmentVariablesObj,
|
||||||
|
dns: `${newDeployment.project.name}-${newDeployment.id}`
|
||||||
|
});
|
||||||
|
|
||||||
await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData });
|
await this.db.updateDeploymentById(newDeployment.id, { applicationDeploymentRequestId, applicationDeploymentRequestData });
|
||||||
|
|
||||||
return newDeployment;
|
return newDeployment;
|
||||||
@ -717,6 +816,51 @@ export class Service {
|
|||||||
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
|
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteDeployment (deploymentId: string): Promise<boolean> {
|
||||||
|
const deployment = await this.db.getDeployment({
|
||||||
|
where: {
|
||||||
|
id: deploymentId
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
project: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deployment && deployment.applicationDeploymentRecordId) {
|
||||||
|
// If deployment is current, remove deployment for project subdomain as well
|
||||||
|
if (deployment.isCurrent) {
|
||||||
|
const currentDeploymentURL = `https://${deployment.project.subDomain}`;
|
||||||
|
|
||||||
|
const deploymentRecords = await this.registry.getDeploymentRecordsByFilter({
|
||||||
|
application: deployment.applicationRecordId,
|
||||||
|
url: currentDeploymentURL
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!deploymentRecords.length) {
|
||||||
|
log(`No ApplicationDeploymentRecord found for URL ${currentDeploymentURL} and ApplicationDeploymentRecord id ${deployment.applicationDeploymentRecordId}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId: deploymentRecords[0].id });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.registry.createApplicationDeploymentRemovalRequest({ deploymentId: deployment.applicationDeploymentRecordId });
|
||||||
|
|
||||||
|
await this.db.updateDeploymentById(
|
||||||
|
deployment.id,
|
||||||
|
{
|
||||||
|
status: DeploymentStatus.Deleting,
|
||||||
|
applicationDeploymentRemovalRequestId: result.applicationDeploymentRemovalRequestId,
|
||||||
|
applicationDeploymentRemovalRequestData: result.applicationDeploymentRemovalRequestData
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (result !== undefined || result !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async addDomain (
|
async addDomain (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
data: { name: string }
|
data: { name: string }
|
||||||
|
@ -38,6 +38,13 @@ export interface AppDeploymentRecordAttributes {
|
|||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppDeploymentRemovalRecordAttributes {
|
||||||
|
deployment: string;
|
||||||
|
request: string;
|
||||||
|
type: "ApplicationDeploymentRemovalRecord";
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface RegistryRecord {
|
interface RegistryRecord {
|
||||||
id: string;
|
id: string;
|
||||||
names: string[] | null;
|
names: string[] | null;
|
||||||
@ -50,3 +57,7 @@ interface RegistryRecord {
|
|||||||
export interface AppDeploymentRecord extends RegistryRecord {
|
export interface AppDeploymentRecord extends RegistryRecord {
|
||||||
attributes: AppDeploymentRecordAttributes;
|
attributes: AppDeploymentRecordAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppDeploymentRemovalRecord extends RegistryRecord {
|
||||||
|
attributes: AppDeploymentRemovalRecordAttributes;
|
||||||
|
}
|
||||||
|
@ -7,7 +7,7 @@ import { Registry } from '@snowballtools/laconic-sdk';
|
|||||||
import { Config } from '../src/config';
|
import { Config } from '../src/config';
|
||||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||||
import { getConfig } from '../src/utils';
|
import { getConfig } from '../src/utils';
|
||||||
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment';
|
||||||
|
|
||||||
const log = debug('snowball:publish-deploy-records');
|
const log = debug('snowball:publish-deploy-records');
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ async function main () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for await (const deployment of deployments) {
|
for await (const deployment of deployments) {
|
||||||
const url = `${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
|
const url = `https://${deployment.project.name}-${deployment.id}.${misc.projectDomain}`;
|
||||||
|
|
||||||
const applicationDeploymentRecord = {
|
const applicationDeploymentRecord = {
|
||||||
type: 'ApplicationDeploymentRecord',
|
type: 'ApplicationDeploymentRecord',
|
||||||
@ -71,6 +71,21 @@ async function main () {
|
|||||||
registryConfig.fee
|
registryConfig.fee
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Remove deployment for project subdomain if deployment is for production environment
|
||||||
|
if (deployment.environment === Environment.Production) {
|
||||||
|
applicationDeploymentRecord.url = `https://${deployment.project.subDomain}`
|
||||||
|
|
||||||
|
await registry.setRecord(
|
||||||
|
{
|
||||||
|
privateKey: registryConfig.privateKey,
|
||||||
|
record: applicationDeploymentRecord,
|
||||||
|
bondId: registryConfig.bondId
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
registryConfig.fee
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
log('Application deployment record data:', applicationDeploymentRecord);
|
log('Application deployment record data:', applicationDeploymentRecord);
|
||||||
log(`Application deployment record published: ${result.data.id}`);
|
log(`Application deployment record published: ${result.data.id}`);
|
||||||
}
|
}
|
||||||
|
67
packages/backend/test/publish-deployment-removal-records.ts
Normal file
67
packages/backend/test/publish-deployment-removal-records.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import debug from 'debug';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { Registry } from '@cerc-io/laconic-sdk';
|
||||||
|
|
||||||
|
import { Config } from '../src/config';
|
||||||
|
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||||
|
import { getConfig } from '../src/utils';
|
||||||
|
import { Deployment, DeploymentStatus } from '../src/entity/Deployment';
|
||||||
|
|
||||||
|
const log = debug('snowball:publish-deployment-removal-records');
|
||||||
|
|
||||||
|
async function main () {
|
||||||
|
const { registryConfig, database, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||||
|
|
||||||
|
const registry = new Registry(
|
||||||
|
registryConfig.gqlEndpoint,
|
||||||
|
registryConfig.restEndpoint,
|
||||||
|
registryConfig.chainId
|
||||||
|
);
|
||||||
|
|
||||||
|
const dataSource = new DataSource({
|
||||||
|
type: 'better-sqlite3',
|
||||||
|
database: database.dbPath,
|
||||||
|
synchronize: true,
|
||||||
|
entities: [path.join(__dirname, '../src/entity/*')]
|
||||||
|
});
|
||||||
|
|
||||||
|
await dataSource.initialize();
|
||||||
|
|
||||||
|
const deploymentRepository = dataSource.getRepository(Deployment);
|
||||||
|
const deployments = await deploymentRepository.find({
|
||||||
|
relations: {
|
||||||
|
project: true
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
status: DeploymentStatus.Deleting
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const deployment of deployments) {
|
||||||
|
const applicationDeploymentRemovalRecord = {
|
||||||
|
type: "ApplicationDeploymentRemovalRecord",
|
||||||
|
version: "1.0.0",
|
||||||
|
deployment: deployment.applicationDeploymentRecordId,
|
||||||
|
request: deployment.applicationDeploymentRemovalRequestId,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await registry.setRecord(
|
||||||
|
{
|
||||||
|
privateKey: registryConfig.privateKey,
|
||||||
|
record: applicationDeploymentRemovalRecord,
|
||||||
|
bondId: registryConfig.bondId
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
registryConfig.fee
|
||||||
|
);
|
||||||
|
|
||||||
|
log('Application deployment removal record data:', applicationDeploymentRemovalRecord);
|
||||||
|
log(`Application deployment removal record published: ${result.data.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
log(err);
|
||||||
|
});
|
@ -53,6 +53,16 @@ export const ProjectCard = ({
|
|||||||
navigate(`projects/${project.id}`);
|
navigate(`projects/${project.id}`);
|
||||||
}, [project.id, navigate]);
|
}, [project.id, navigate]);
|
||||||
|
|
||||||
|
const navigateToSettingsOnClick = useCallback(
|
||||||
|
(
|
||||||
|
e: React.MouseEvent<HTMLLIElement> | React.MouseEvent<HTMLButtonElement>,
|
||||||
|
) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigate(`projects/${project.id}/settings`);
|
||||||
|
},
|
||||||
|
[project.id, navigate],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
@ -92,8 +102,15 @@ export const ProjectCard = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</MenuHandler>
|
</MenuHandler>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem>Project settings</MenuItem>
|
<MenuItem onClick={navigateToSettingsOnClick}>
|
||||||
<MenuItem className="text-red-500">Delete project</MenuItem>
|
Project settings
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
className="text-red-500"
|
||||||
|
onClick={navigateToSettingsOnClick}
|
||||||
|
>
|
||||||
|
Delete project
|
||||||
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
|
@ -155,13 +155,13 @@ export const RepositoryList = () => {
|
|||||||
{Boolean(repositoryDetails.length) ? (
|
{Boolean(repositoryDetails.length) ? (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{repositoryDetails.map((repo, index) => (
|
{repositoryDetails.map((repo, index) => (
|
||||||
<>
|
<div key={index}>
|
||||||
<ProjectRepoCard repository={repo} key={index} />
|
<ProjectRepoCard repository={repo} />
|
||||||
{/* Horizontal line */}
|
{/* Horizontal line */}
|
||||||
{index !== repositoryDetails.length - 1 && (
|
{index !== repositoryDetails.length - 1 && (
|
||||||
<div className="border-b border-border-separator/[0.06] w-full" />
|
<div className="border-b border-border-separator/[0.06] w-full" />
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -38,6 +38,7 @@ const STATUS_COLORS: {
|
|||||||
[DeploymentStatus.Building]: 'emphasized',
|
[DeploymentStatus.Building]: 'emphasized',
|
||||||
[DeploymentStatus.Ready]: 'positive',
|
[DeploymentStatus.Ready]: 'positive',
|
||||||
[DeploymentStatus.Error]: 'negative',
|
[DeploymentStatus.Error]: 'negative',
|
||||||
|
[DeploymentStatus.Deleting]: 'neutral',
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeploymentDetailsCard = ({
|
const DeploymentDetailsCard = ({
|
||||||
@ -48,7 +49,7 @@ const DeploymentDetailsCard = ({
|
|||||||
prodBranchDomains,
|
prodBranchDomains,
|
||||||
}: DeployDetailsCardProps) => {
|
}: DeployDetailsCardProps) => {
|
||||||
const getIconByDeploymentStatus = (status: DeploymentStatus) => {
|
const getIconByDeploymentStatus = (status: DeploymentStatus) => {
|
||||||
if (status === DeploymentStatus.Building) {
|
if (status === DeploymentStatus.Building || status === DeploymentStatus.Deleting) {
|
||||||
return <LoadingIcon className="animate-spin" />;
|
return <LoadingIcon className="animate-spin" />;
|
||||||
}
|
}
|
||||||
if (status === DeploymentStatus.Ready) {
|
if (status === DeploymentStatus.Ready) {
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
RefreshIcon,
|
RefreshIcon,
|
||||||
RocketIcon,
|
RocketIcon,
|
||||||
UndoIcon,
|
UndoIcon,
|
||||||
|
CrossCircleIcon,
|
||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
@ -79,6 +80,16 @@ export const DeploymentMenu = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteDeployment = async () => {
|
||||||
|
const isDeleted = await client.deleteDeployment(deployment.id);
|
||||||
|
if (isDeleted) {
|
||||||
|
await onUpdate();
|
||||||
|
toast.success('Deleted deployment');
|
||||||
|
} else {
|
||||||
|
toast.error('Unable to delete deployment');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={cn('max-w-[32px]', className)} {...props}>
|
<div className={cn('max-w-[32px]', className)} {...props}>
|
||||||
@ -147,6 +158,12 @@ export const DeploymentMenu = ({
|
|||||||
>
|
>
|
||||||
<UndoIcon /> Rollback to this version
|
<UndoIcon /> Rollback to this version
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||||
|
onClick={() => deleteDeployment()}
|
||||||
|
>
|
||||||
|
<CrossCircleIcon /> Delete deployment
|
||||||
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,7 +107,6 @@ export const UserSelect = ({ options, value }: UserSelectProps) => {
|
|||||||
ref: inputWrapperRef,
|
ref: inputWrapperRef,
|
||||||
suppressRefError: true,
|
suppressRefError: true,
|
||||||
})}
|
})}
|
||||||
ref={inputWrapperRef}
|
|
||||||
onClick={() => !dropdownOpen && openMenu()}
|
onClick={() => !dropdownOpen && openMenu()}
|
||||||
className="cursor-pointer relative py-2 pl-2 pr-4 flex min-w-[200px] w-full items-center justify-between rounded-xl bg-surface-card shadow-sm"
|
className="cursor-pointer relative py-2 pl-2 pr-4 flex min-w-[200px] w-full items-center justify-between rounded-xl bg-surface-card shadow-sm"
|
||||||
>
|
>
|
||||||
|
0
packages/frontend/src/context/Web3ModalProvider.tsx
Normal file
0
packages/frontend/src/context/Web3ModalProvider.tsx
Normal file
@ -21,7 +21,7 @@ const root = ReactDOM.createRoot(
|
|||||||
|
|
||||||
assert(
|
assert(
|
||||||
import.meta.env.VITE_SERVER_URL,
|
import.meta.env.VITE_SERVER_URL,
|
||||||
'REACT_APP_SERVER_URL is not set in env',
|
'VITE_SERVER_URL is not set in env',
|
||||||
);
|
);
|
||||||
const gqlEndpoint = `${import.meta.env.VITE_SERVER_URL}/${SERVER_GQL_PATH}`;
|
const gqlEndpoint = `${import.meta.env.VITE_SERVER_URL}/${SERVER_GQL_PATH}`;
|
||||||
|
|
||||||
|
@ -276,6 +276,17 @@ export class GQLClient {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteDeployment (deploymentId: string): Promise<types.DeleteDeploymentResponse> {
|
||||||
|
const { data } = await this.client.mutate({
|
||||||
|
mutation: mutations.deleteDeployment,
|
||||||
|
variables: {
|
||||||
|
deploymentId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
async addDomain (projectId: string, data: types.AddDomainInput): Promise<types.AddDomainResponse> {
|
async addDomain (projectId: string, data: types.AddDomainInput): Promise<types.AddDomainResponse> {
|
||||||
const result = await this.client.mutate({
|
const result = await this.client.mutate({
|
||||||
mutation: mutations.addDomain,
|
mutation: mutations.addDomain,
|
||||||
|
@ -82,6 +82,12 @@ mutation ($projectId: String! ,$deploymentId: String!) {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const deleteDeployment = gql`
|
||||||
|
mutation ($deploymentId: String!) {
|
||||||
|
deleteDeployment(deploymentId: $deploymentId)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const addDomain = gql`
|
export const addDomain = gql`
|
||||||
mutation ($projectId: String!, $data: AddDomainInput!) {
|
mutation ($projectId: String!, $data: AddDomainInput!) {
|
||||||
addDomain(projectId: $projectId, data: $data)
|
addDomain(projectId: $projectId, data: $data)
|
||||||
|
@ -21,6 +21,7 @@ export enum DeploymentStatus {
|
|||||||
Building = 'Building',
|
Building = 'Building',
|
||||||
Ready = 'Ready',
|
Ready = 'Ready',
|
||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
|
Deleting = 'Deleting'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DomainStatus {
|
export enum DomainStatus {
|
||||||
@ -269,6 +270,10 @@ export type RollbackDeploymentResponse = {
|
|||||||
rollbackDeployment: boolean
|
rollbackDeployment: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DeleteDeploymentResponse = {
|
||||||
|
deleteDeployment: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type AddDomainInput = {
|
export type AddDomainInput = {
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user