diff --git a/README.md b/README.md index d5129ff..557b96e 100644 --- a/README.md +++ b/README.md @@ -99,25 +99,13 @@ 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) ```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 # 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) - + - Run the script to create bond, reserve the authority and set authority bond ```bash @@ -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 ```env - REACT_APP_GITHUB_CLIENT_ID = + VITE_GITHUB_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 - 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 - 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_PWA_TEMPLATE_REPO = 'cerc-io/test-progressive-web-app' # Set actual owner/name of the template repo that will be used for creating new repo + VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'cerc-io/image-upload-pwa-example' # Set actual owner/name of the template repo that will be used for creating new repo ``` - Production @@ -172,17 +160,17 @@ Let us assume the following domains for backend and frontend - Set the following values in [.env](packages/frontend/.env) file ```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 - Create a project and add information to use wallet connect SDK - Add project name and select project type as `App` - Set project home page URL to `https://dashboard.snowballtools.com` - - On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file + - On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file ```env - REACT_APP_WALLET_CONNECT_ID = + VITE_WALLET_CONNECT_ID = ``` - 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` ```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. - Create a project and add information to use wallet connect SDK - Add project name and select project type as `App` - Project home page URL is not required to be set - - On creation of project, use the `Project ID` and set it in `REACT_APP_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file + - On creation of project, use the `Project ID` and set it in `VITE_WALLET_CONNECT_ID` in [.env](packages/frontend/.env) file ```env - REACT_APP_WALLET_CONNECT_ID = + VITE_WALLET_CONNECT_ID = ``` - The React application will be running in `http://localhost:3000/` diff --git a/packages/backend/package.json b/packages/backend/package.json index fdd5b24..c352dd0 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -43,6 +43,7 @@ "lint": "tsc --noEmit", "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-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:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts" }, diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index a9ba525..b5586b8 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -436,6 +436,19 @@ export class Database { return Boolean(updateResult.affected); } + async deleteDeploymentById (deploymentId: string): Promise { + 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): Promise { const projectRepository = this.dataSource.getRepository(Project); diff --git a/packages/backend/src/entity/Deployment.ts b/packages/backend/src/entity/Deployment.ts index 8e0a324..4535ea2 100644 --- a/packages/backend/src/entity/Deployment.ts +++ b/packages/backend/src/entity/Deployment.ts @@ -6,13 +6,14 @@ import { UpdateDateColumn, ManyToOne, OneToOne, - JoinColumn + JoinColumn, + DeleteDateColumn } from 'typeorm'; import { Project } from './Project'; import { Domain } from './Domain'; import { User } from './User'; -import { AppDeploymentRecordAttributes } from '../types'; +import { AppDeploymentRecordAttributes, AppDeploymentRemovalRecordAttributes } from '../types'; export enum Environment { Production = 'Production', @@ -24,6 +25,7 @@ export enum DeploymentStatus { Building = 'Building', Ready = 'Ready', Error = 'Error', + Deleting = 'Deleting', } export interface ApplicationDeploymentRequest { @@ -35,6 +37,18 @@ export interface ApplicationDeploymentRequest { meta: string; } +export interface ApplicationDeploymentRemovalRequest { + type: string; + version: string; + deployment: string; +} + +export interface ApplicationDeploymentRemovalRequest { + type: string; + version: string; + deployment: string; +} + export interface ApplicationRecord { type: string; version: string; @@ -98,6 +112,18 @@ export class Deployment { @Column('simple-json', { nullable: true }) applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null; + + @Column('varchar', { nullable: true }) + applicationDeploymentRemovalRequestId!: string | null; + + @Column('simple-json', { nullable: true }) + applicationDeploymentRemovalRequestData!: ApplicationDeploymentRemovalRequest | null; + + @Column('varchar', { nullable: true }) + applicationDeploymentRemovalRecordId!: string | null; + + @Column('simple-json', { nullable: true }) + applicationDeploymentRemovalRecordData!: AppDeploymentRemovalRecordAttributes | null; @Column({ enum: Environment @@ -121,4 +147,7 @@ export class Deployment { @UpdateDateColumn() updatedAt!: Date; + + @DeleteDateColumn() + deletedAt!: Date | null; } diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index 92cdbfe..fee386c 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -9,16 +9,19 @@ import { RegistryConfig } from './config'; import { ApplicationRecord, Deployment, - ApplicationDeploymentRequest + ApplicationDeploymentRequest, + ApplicationDeploymentRemovalRequest } from './entity/Deployment'; -import { AppDeploymentRecord, PackageJSON } from './types'; +import { AppDeploymentRecord, AppDeploymentRemovalRecord, PackageJSON } from './types'; import { sleep } from './utils'; const log = debug('snowball:registry'); const APP_RECORD_TYPE = 'ApplicationRecord'; const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest'; +const APP_DEPLOYMENT_REMOVAL_REQUEST_TYPE = 'ApplicationDeploymentRemovalRequest'; const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord'; +const APP_DEPLOYMENT_REMOVAL_RECORD_TYPE = 'ApplicationDeploymentRemovalRecord'; const SLEEP_DURATION = 1000; // TODO: Move registry code to laconic-sdk/watcher-ts @@ -229,6 +232,74 @@ export class Registry { ); } + /** + * Fetch ApplicationDeploymentRecords by filter + */ + async getDeploymentRecordsByFilter (filter: { [key: string]: any }): Promise { + return this.registry.queryRecords( + { + type: APP_DEPLOYMENT_RECORD_TYPE, + ...filter + }, + true + ); + } + + /** + * Fetch ApplicationDeploymentRemovalRecords for deployments + */ + async getDeploymentRemovalRecords ( + deployments: Deployment[] + ): Promise { + // 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 { assert(this.registryConfig.authority, "Authority doesn't exist"); return `crn://${this.registryConfig.authority}/applications/${appName}`; diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index e2e3339..5e40c74 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -255,6 +255,20 @@ export const createResolvers = async (service: Service): Promise => { } }, + deleteDeployment: async ( + _: any, + { + deploymentId + }: { deploymentId: string; } + ) => { + try { + return await service.deleteDeployment(deploymentId); + } catch (err) { + log(err); + return false; + } + }, + addDomain: async ( _: any, { projectId, data }: { projectId: string; data: { name: string } } diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index c22db8d..7aa4799 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -19,6 +19,7 @@ enum DeploymentStatus { Building Ready Error + Deleting } enum DomainStatus { @@ -209,6 +210,7 @@ type Mutation { deleteProject(projectId: String!): Boolean! deleteDomain(domainId: String!): Boolean! rollbackDeployment(projectId: String!, deploymentId: String!): Boolean! + deleteDeployment(deploymentId: String!): Boolean! addDomain(projectId: String!, data: AddDomainInput!): Boolean! updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean! authenticateGitHub(code: String!): AuthResult! diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index c87f125..aa6b598 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -15,13 +15,16 @@ import { Permission, ProjectMember } from './entity/ProjectMember'; import { User } from './entity/User'; import { Registry } from './registry'; import { GitHubConfig, RegistryConfig } from './config'; -import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types'; +import { AppDeploymentRecord, AppDeploymentRemovalRecord, GitPushEventPayload, PackageJSON } from './types'; import { Role } from './entity/UserOrganization'; const log = debug('snowball:service'); 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 { gitHubConfig: GitHubConfig; registryConfig: RegistryConfig; @@ -49,6 +52,8 @@ export class Service { init (): void { // Start check for ApplicationDeploymentRecords asynchronously this.checkDeployRecordsAndUpdate(); + // Start check for ApplicationDeploymentRemovalRecords asynchronously + this.checkDeploymentRemovalRecordsAndUpdate(); } /** @@ -60,14 +65,13 @@ export class Service { /** * 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 { // Fetch deployments in building state const deployments = await this.db.getDeployments({ where: { 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` ); + // 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 const records = await this.registry.getDeploymentRecords(deployments); log(`Found ${records.length} ApplicationDeploymentRecords`); @@ -91,6 +117,38 @@ export class Service { }, this.config.registryConfig.fetchDeploymentRecordDelay); } + /** + * Checks for ApplicationDeploymentRemovalRecord and remove corresponding deployments + * Continues check in loop after a delay of registryConfig.fetchDeploymentRecordDelay + */ + async checkDeploymentRemovalRecordsAndUpdate (): Promise { + // 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 */ @@ -153,6 +211,45 @@ export class Service { await Promise.all(deploymentUpdatePromises); } + /** + * Delete deployments with ApplicationDeploymentRemovalRecord data + */ + async deleteDeploymentsWithRecordData ( + records: AppDeploymentRemovalRecord[], + deployments: Deployment[], + ): Promise { + 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 { return this.db.getUser({ where: { @@ -479,17 +576,10 @@ export class Service { return acc; }, {} 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 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, @@ -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 }); return newDeployment; @@ -717,6 +816,51 @@ export class Service { return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate; } + async deleteDeployment (deploymentId: string): Promise { + 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 ( projectId: string, data: { name: string } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 8094134..fecb4a6 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -38,6 +38,13 @@ export interface AppDeploymentRecordAttributes { version: string; } +export interface AppDeploymentRemovalRecordAttributes { + deployment: string; + request: string; + type: "ApplicationDeploymentRemovalRecord"; + version: string; +} + interface RegistryRecord { id: string; names: string[] | null; @@ -50,3 +57,7 @@ interface RegistryRecord { export interface AppDeploymentRecord extends RegistryRecord { attributes: AppDeploymentRecordAttributes; } + +export interface AppDeploymentRemovalRecord extends RegistryRecord { + attributes: AppDeploymentRemovalRecordAttributes; +} diff --git a/packages/backend/test/publish-deploy-records.ts b/packages/backend/test/publish-deploy-records.ts index 7dfe3d2..f0fc22e 100644 --- a/packages/backend/test/publish-deploy-records.ts +++ b/packages/backend/test/publish-deploy-records.ts @@ -7,7 +7,7 @@ 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 } from '../src/entity/Deployment'; +import { Deployment, DeploymentStatus, Environment } from '../src/entity/Deployment'; const log = debug('snowball:publish-deploy-records'); @@ -40,7 +40,7 @@ async function main () { }); 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 = { type: 'ApplicationDeploymentRecord', @@ -71,6 +71,21 @@ async function main () { 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 published: ${result.data.id}`); } diff --git a/packages/backend/test/publish-deployment-removal-records.ts b/packages/backend/test/publish-deployment-removal-records.ts new file mode 100644 index 0000000..d8b026a --- /dev/null +++ b/packages/backend/test/publish-deployment-removal-records.ts @@ -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(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); +}); diff --git a/packages/frontend/src/components/projects/ProjectCard/ProjectCard.tsx b/packages/frontend/src/components/projects/ProjectCard/ProjectCard.tsx index c15d2db..e1987d2 100644 --- a/packages/frontend/src/components/projects/ProjectCard/ProjectCard.tsx +++ b/packages/frontend/src/components/projects/ProjectCard/ProjectCard.tsx @@ -53,6 +53,16 @@ export const ProjectCard = ({ navigate(`projects/${project.id}`); }, [project.id, navigate]); + const navigateToSettingsOnClick = useCallback( + ( + e: React.MouseEvent | React.MouseEvent, + ) => { + e.stopPropagation(); + navigate(`projects/${project.id}/settings`); + }, + [project.id, navigate], + ); + return (
- Project settings - Delete project + + Project settings + + + Delete project +
diff --git a/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx b/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx index 3e6adee..8f7c781 100644 --- a/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx +++ b/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx @@ -155,13 +155,13 @@ export const RepositoryList = () => { {Boolean(repositoryDetails.length) ? (
{repositoryDetails.map((repo, index) => ( - <> - +
+ {/* Horizontal line */} {index !== repositoryDetails.length - 1 && (
)} - +
))}
) : ( diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx index 4c55e24..0e605bc 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx @@ -38,6 +38,7 @@ const STATUS_COLORS: { [DeploymentStatus.Building]: 'emphasized', [DeploymentStatus.Ready]: 'positive', [DeploymentStatus.Error]: 'negative', + [DeploymentStatus.Deleting]: 'neutral', }; const DeploymentDetailsCard = ({ @@ -48,7 +49,7 @@ const DeploymentDetailsCard = ({ prodBranchDomains, }: DeployDetailsCardProps) => { const getIconByDeploymentStatus = (status: DeploymentStatus) => { - if (status === DeploymentStatus.Building) { + if (status === DeploymentStatus.Building || status === DeploymentStatus.Deleting) { return ; } if (status === DeploymentStatus.Ready) { diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx index c2b62fd..5dbb95a 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentMenu.tsx @@ -9,6 +9,7 @@ import { RefreshIcon, RocketIcon, UndoIcon, + CrossCircleIcon, } from 'components/shared/CustomIcon'; import { 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 ( <>
@@ -147,6 +158,12 @@ export const DeploymentMenu = ({ > Rollback to this version + deleteDeployment()} + > + Delete deployment +
diff --git a/packages/frontend/src/components/shared/UserSelect/UserSelect.tsx b/packages/frontend/src/components/shared/UserSelect/UserSelect.tsx index 139f6a1..d9faa10 100644 --- a/packages/frontend/src/components/shared/UserSelect/UserSelect.tsx +++ b/packages/frontend/src/components/shared/UserSelect/UserSelect.tsx @@ -107,7 +107,6 @@ export const UserSelect = ({ options, value }: UserSelectProps) => { ref: inputWrapperRef, suppressRefError: true, })} - ref={inputWrapperRef} 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" > diff --git a/packages/frontend/src/context/Web3ModalProvider.tsx b/packages/frontend/src/context/Web3ModalProvider.tsx new file mode 100644 index 0000000..e69de29 diff --git a/packages/frontend/src/index.tsx b/packages/frontend/src/index.tsx index 5dc1a35..0d9e662 100644 --- a/packages/frontend/src/index.tsx +++ b/packages/frontend/src/index.tsx @@ -21,7 +21,7 @@ const root = ReactDOM.createRoot( assert( 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}`; diff --git a/packages/gql-client/src/client.ts b/packages/gql-client/src/client.ts index 74c7683..f238869 100644 --- a/packages/gql-client/src/client.ts +++ b/packages/gql-client/src/client.ts @@ -276,6 +276,17 @@ export class GQLClient { return data; } + async deleteDeployment (deploymentId: string): Promise { + const { data } = await this.client.mutate({ + mutation: mutations.deleteDeployment, + variables: { + deploymentId + } + }); + + return data; + } + async addDomain (projectId: string, data: types.AddDomainInput): Promise { const result = await this.client.mutate({ mutation: mutations.addDomain, diff --git a/packages/gql-client/src/mutations.ts b/packages/gql-client/src/mutations.ts index fd7a9c2..be8d96d 100644 --- a/packages/gql-client/src/mutations.ts +++ b/packages/gql-client/src/mutations.ts @@ -82,6 +82,12 @@ mutation ($projectId: String! ,$deploymentId: String!) { } `; +export const deleteDeployment = gql` +mutation ($deploymentId: String!) { + deleteDeployment(deploymentId: $deploymentId) +} +`; + export const addDomain = gql` mutation ($projectId: String!, $data: AddDomainInput!) { addDomain(projectId: $projectId, data: $data) diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index 71c44c0..ac6481e 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -21,6 +21,7 @@ export enum DeploymentStatus { Building = 'Building', Ready = 'Ready', Error = 'Error', + Deleting = 'Deleting' } export enum DomainStatus { @@ -269,6 +270,10 @@ export type RollbackDeploymentResponse = { rollbackDeployment: boolean } +export type DeleteDeploymentResponse = { + deleteDeployment: boolean +} + export type AddDomainInput = { name: string }