From b007286f4ad6716554c99214fdc5004fedb3652e Mon Sep 17 00:00:00 2001 From: Nabarun Gogoi Date: Fri, 16 Feb 2024 15:05:52 +0530 Subject: [PATCH 01/12] Add script to publish dummy `ApplicationDeploymentRecord` (#72) * Add script to publish application deployment record * Refactor publish deploy records script * Assert if name doesn't exist in package.json * Disable import environment variables button --------- Co-authored-by: neeraj --- packages/backend/package.json | 1 + packages/backend/src/database.ts | 15 ++-- packages/backend/src/entity/Deployment.ts | 6 +- packages/backend/src/registry.ts | 5 +- .../backend/test/fixtures/deployments.json | 14 ++-- .../backend/test/publish-deploy-records.ts | 77 +++++++++++++++++++ .../id/settings/EnvironmentVariables.tsx | 3 +- 7 files changed, 102 insertions(+), 19 deletions(-) create mode 100644 packages/backend/test/publish-deploy-records.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index 75ba8d67..c28f72c7 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -37,6 +37,7 @@ "format": "prettier --write .", "format:check": "prettier --check .", "registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts", + "registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts", "db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts", "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 81c9fbd3..a2959773 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -127,9 +127,7 @@ export class Database { } async getDeploymentsByProjectId (projectId: string): Promise { - const deploymentRepository = this.dataSource.getRepository(Deployment); - - const deployments = await deploymentRepository.find({ + const deployments = await this.getDeployments({ relations: { project: true, domain: true, @@ -148,6 +146,13 @@ export class Database { return deployments; } + async getDeployments (options: FindManyOptions): Promise { + const deploymentRepository = this.dataSource.getRepository(Deployment); + const deployments = await deploymentRepository.find(options); + + return deployments; + } + async getDeployment (options: FindOneOptions): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); const deployment = await deploymentRepository.findOne(options); @@ -166,12 +171,10 @@ export class Database { const deploymentRepository = this.dataSource.getRepository(Deployment); const id = nanoid(); - const url = `${data.project!.name}-${id}.${PROJECT_DOMAIN}`; const updatedData = { ...data, - id, - url + id }; const deployment = await deploymentRepository.save(updatedData); diff --git a/packages/backend/src/entity/Deployment.ts b/packages/backend/src/entity/Deployment.ts index 2ccb1c0b..76238f32 100644 --- a/packages/backend/src/entity/Deployment.ts +++ b/packages/backend/src/entity/Deployment.ts @@ -28,7 +28,7 @@ export enum DeploymentStatus { export interface ApplicationRecord { type: string; version:string - name?: string + name: string description?: string homepage?: string license?: string @@ -65,8 +65,8 @@ export class Deployment { @Column('varchar') commitMessage!: string; - @Column('varchar') - url!: string; + @Column('varchar', { nullable: true }) + url!: string | null; @Column('varchar') registryRecordId!: string; diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index 5c17871a..02dc924b 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -36,6 +36,7 @@ export class Registry { appType: string, repoUrl: string }): Promise<{registryRecordId: string, registryRecordData: ApplicationRecord}> { + assert(packageJSON.name, "name field doesn't exist in package.json"); // 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 @@ -58,7 +59,7 @@ export class Registry { repository_ref: commitHash, repository: [repoUrl], app_type: appType, - ...(packageJSON.name && { name: packageJSON.name }), + name: packageJSON.name, ...(packageJSON.description && { description: packageJSON.description }), ...(packageJSON.homepage && { homepage: packageJSON.homepage }), ...(packageJSON.license && { license: packageJSON.license }), @@ -79,7 +80,7 @@ export class Registry { log('Application record data:', applicationRecord); // TODO: Discuss computation of CRN - const crn = this.getCrn(packageJSON.name ?? ''); + const crn = this.getCrn(packageJSON.name); log(`Setting name: ${crn} for record ID: ${result.data.id}`); await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee); diff --git a/packages/backend/test/fixtures/deployments.json b/packages/backend/test/fixtures/deployments.json index e0900921..25176713 100644 --- a/packages/backend/test/fixtures/deployments.json +++ b/packages/backend/test/fixtures/deployments.json @@ -4,7 +4,7 @@ "domainIndex":0, "createdByIndex": 0, "id":"ffhae3zq", - "status": "Building", + "status": "Ready", "environment": "Production", "isCurrent": true, "registryRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", @@ -34,7 +34,7 @@ "domainIndex":2, "createdByIndex": 0, "id":"qmgekyte", - "status": "Error", + "status": "Ready", "environment": "Development", "isCurrent": false, "registryRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", @@ -64,7 +64,7 @@ "domainIndex":3, "createdByIndex": 1, "id":"eO8cckxk", - "status": "Building", + "status": "Ready", "environment": "Production", "isCurrent": true, "registryRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", @@ -94,7 +94,7 @@ "domainIndex":5, "createdByIndex": 1, "id":"hwwr6sbx", - "status": "Error", + "status": "Ready", "environment": "Development", "isCurrent": false, "registryRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi", @@ -109,7 +109,7 @@ "domainIndex":9, "createdByIndex": 2, "id":"ndxje48a", - "status": "Building", + "status": "Ready", "environment": "Production", "isCurrent": true, "registryRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", @@ -139,7 +139,7 @@ "domainIndex":8, "createdByIndex": 2, "id":"b4bpthjr", - "status": "Error", + "status": "Ready", "environment": "Development", "isCurrent": false, "registryRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", @@ -154,7 +154,7 @@ "domainIndex": 6, "createdByIndex": 2, "id":"b4bpthjr", - "status": "Building", + "status": "Ready", "environment": "Production", "isCurrent": true, "registryRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", diff --git a/packages/backend/test/publish-deploy-records.ts b/packages/backend/test/publish-deploy-records.ts new file mode 100644 index 00000000..6f2d26b0 --- /dev/null +++ b/packages/backend/test/publish-deploy-records.ts @@ -0,0 +1,77 @@ +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, PROJECT_DOMAIN } from '../src/constants'; +import { getConfig } from '../src/utils'; +import { Deployment, DeploymentStatus } from '../src/entity/Deployment'; + +const log = debug('snowball:publish-deploy-records'); + +async function main () { + const { registryConfig, database } = 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.Building + } + }); + + for await (const deployment of deployments) { + const url = `${deployment.project.name}-${deployment.id}.${PROJECT_DOMAIN}`; + + const applicationDeploymentRecord = { + type: 'ApplicationDeploymentRecord', + version: '0.0.1', + name: deployment.registryRecordData.name, + application: deployment.registryRecordId, + + // TODO: Create DNS record + dns: 'bafyreihlymqggsgqiqawvehkpr2imt4l3u6q7um7xzjrux5rhsvwnuyewm', + + // Using dummy values + meta: JSON.stringify({ + config: 'da39a3ee5e6b4b0d3255bfef95601890afd80709', + so: '66fcfa49a1664d4cb4ce4f72c1c0e151' + }), + + request: deployment.project.registryRecordId, + url + }; + + const result = 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}`); + } +} + +main().catch((err) => { + log(err); +}); diff --git a/packages/frontend/src/pages/org-slug/projects/id/settings/EnvironmentVariables.tsx b/packages/frontend/src/pages/org-slug/projects/id/settings/EnvironmentVariables.tsx index 0ae3932b..c0eb59b9 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/settings/EnvironmentVariables.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/settings/EnvironmentVariables.tsx @@ -171,7 +171,8 @@ export const EnvironmentVariablesTabPanel = () => { > + Add variable - From ce55fe62d8d4d55fe276b078c81d965e84579639 Mon Sep 17 00:00:00 2001 From: Nabarun Gogoi Date: Mon, 19 Feb 2024 13:43:29 +0530 Subject: [PATCH 02/12] Check for `ApplicationDeploymentRecord` and update deployments (#73) * Add skeleton and TODOs for polling deployment records * Add method implementations for fetching deployment records * Handle if deployment url is not set * Add logs after getting ApplicationDeploymentRecord * Add application deployment record to deployment entity * Change type of application deployment record data * Fetch delay to check deployment records from config * Update isCurrent after deployment record received --------- Co-authored-by: neeraj --- README.md | 4 +- packages/backend/environments/local.toml | 1 + packages/backend/package.json | 8 +- packages/backend/src/config.ts | 1 + packages/backend/src/database.ts | 23 +-- packages/backend/src/entity/Deployment.ts | 14 +- packages/backend/src/entity/Project.ts | 4 +- packages/backend/src/index.ts | 2 +- packages/backend/src/registry.ts | 33 ++-- packages/backend/src/resolvers.ts | 2 +- packages/backend/src/schema.gql | 2 +- packages/backend/src/service.ts | 151 ++++++++++++++---- packages/backend/src/types.ts | 24 +++ .../backend/test/fixtures/deployments.json | 44 ++--- packages/backend/test/fixtures/projects.json | 20 +-- .../backend/test/publish-deploy-records.ts | 6 +- .../deployments/DeploymentDetailsCard.tsx | 6 +- .../deployments/DeploymentDialogBodyCard.tsx | 8 +- packages/gql-client/src/types.ts | 2 +- 19 files changed, 252 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index 3accec13..c46cbbb3 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ - Load fixtures in database ```bash - yarn db:load:fixtures + yarn test:db:load:fixtures ``` - Set `gitHub.oAuth.clientId` and `gitHub.oAuth.clientSecret` in backend [config file](packages/backend/environments/local.toml) @@ -66,7 +66,7 @@ - Run the script to create bond, reserve the authority and set authority bond ```bash - yarn registry:init + yarn test:registry:init # snowball:initialize-registry bondId: 6af0ab81973b93d3511ae79841756fb5da3fd2f70ea1279e81fae7c9b19af6c4 +0ms ``` diff --git a/packages/backend/environments/local.toml b/packages/backend/environments/local.toml index 7ca5521f..b5b5d03a 100644 --- a/packages/backend/environments/local.toml +++ b/packages/backend/environments/local.toml @@ -13,6 +13,7 @@ clientSecret = "" [registryConfig] + fetchDeploymentRecordDelay = 5000 restEndpoint = "http://localhost:1317" gqlEndpoint = "http://localhost:9473/api" chainId = "laconic_9000-1" diff --git a/packages/backend/package.json b/packages/backend/package.json index c28f72c7..83138f1d 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -36,10 +36,10 @@ "lint": "eslint .", "format": "prettier --write .", "format:check": "prettier --check .", - "registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts", - "registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts", - "db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts", - "db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.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:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts", + "test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts" }, "devDependencies": { "@types/fs-extra": "^11.0.4", diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 50b34145..1a98a657 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -22,6 +22,7 @@ export interface RegistryConfig { chainId: string; privateKey: string; bondId: string; + fetchDeploymentRecordDelay: number; fee: { amount: string; denom: string; diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index a2959773..7c76f928 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -126,8 +126,18 @@ export class Database { return projects; } + /** + * Get deployments with specified filter + */ + async getDeployments (options: FindManyOptions): Promise { + const deploymentRepository = this.dataSource.getRepository(Deployment); + const deployments = await deploymentRepository.find(options); + + return deployments; + } + async getDeploymentsByProjectId (projectId: string): Promise { - const deployments = await this.getDeployments({ + return this.getDeployments({ relations: { project: true, domain: true, @@ -142,15 +152,6 @@ export class Database { createdAt: 'DESC' } }); - - return deployments; - } - - async getDeployments (options: FindManyOptions): Promise { - const deploymentRepository = this.dataSource.getRepository(Deployment); - const deployments = await deploymentRepository.find(options); - - return deployments; } async getDeployment (options: FindOneOptions): Promise { @@ -167,7 +168,7 @@ export class Database { return domains; } - async addDeployement (data: DeepPartial): Promise { + async addDeployment (data: DeepPartial): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); const id = nanoid(); diff --git a/packages/backend/src/entity/Deployment.ts b/packages/backend/src/entity/Deployment.ts index 76238f32..62d3f38f 100644 --- a/packages/backend/src/entity/Deployment.ts +++ b/packages/backend/src/entity/Deployment.ts @@ -12,6 +12,7 @@ import { import { Project } from './Project'; import { Domain } from './Domain'; import { User } from './User'; +import { AppDeploymentRecordAttributes } from '../types'; export enum Environment { Production = 'Production', @@ -45,6 +46,9 @@ export class Deployment { @PrimaryColumn('varchar') id!: string; + @Column() + projectId!: string; + @ManyToOne(() => Project, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'projectId' }) project!: Project; @@ -69,10 +73,16 @@ export class Deployment { url!: string | null; @Column('varchar') - registryRecordId!: string; + applicationRecordId!: string; @Column('simple-json') - registryRecordData!: ApplicationRecord; + applicationRecordData!: ApplicationRecord; + + @Column('varchar', { nullable: true }) + applicationDeploymentRecordId!: string | null; + + @Column('simple-json', { nullable: true }) + applicationDeploymentRecordData!: AppDeploymentRecordAttributes | null; @Column({ enum: Environment diff --git a/packages/backend/src/entity/Project.ts b/packages/backend/src/entity/Project.ts index 667d120c..86fa7b3d 100644 --- a/packages/backend/src/entity/Project.ts +++ b/packages/backend/src/entity/Project.ts @@ -53,10 +53,10 @@ export class Project { prodBranch!: string; @Column('varchar', { nullable: true }) - registryRecordId!: string | null; + applicationDeploymentRequestId!: string | null; @Column('simple-json', { nullable: true }) - registryRecordData!: ApplicationDeploymentRequest | null; + applicationDeploymentRequestData!: ApplicationDeploymentRequest | null; @Column('text', { default: '' }) description!: string; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index d29f3c32..7a871a28 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -31,7 +31,7 @@ export const main = async (): Promise => { await db.init(); const registry = new Registry(registryConfig); - const service = new Service({ gitHubConfig: gitHub }, db, app, registry); + const service = new Service({ gitHubConfig: gitHub, registryConfig }, db, app, registry); const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString(); const resolvers = await createResolvers(service); diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index 02dc924b..108318bb 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -7,13 +7,14 @@ import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk'; import { RegistryConfig } from './config'; import { ApplicationDeploymentRequest } from './entity/Project'; -import { ApplicationRecord } from './entity/Deployment'; -import { PackageJSON } from './types'; +import { ApplicationRecord, Deployment } from './entity/Deployment'; +import { AppDeploymentRecord, PackageJSON } from './types'; const log = debug('snowball:registry'); const APP_RECORD_TYPE = 'ApplicationRecord'; -const DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRequest'; +const APP_DEPLOYMENT_REQUEST_TYPE = 'ApplicationDeploymentRequest'; +const APP_DEPLOYMENT_RECORD_TYPE = 'ApplicationDeploymentRecord'; // TODO: Move registry code to laconic-sdk/watcher-ts export class Registry { @@ -35,7 +36,7 @@ export class Registry { commitHash: string, appType: string, repoUrl: string - }): Promise<{registryRecordId: string, registryRecordData: ApplicationRecord}> { + }): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> { assert(packageJSON.name, "name field doesn't exist in package.json"); // 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 @@ -87,7 +88,7 @@ export class Registry { await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.app_version}` }, this.registryConfig.privateKey, this.registryConfig.fee); await this.registry.setName({ cid: result.data.id, crn: `${crn}@${applicationRecord.repository_ref}` }, this.registryConfig.privateKey, this.registryConfig.fee); - return { registryRecordId: result.data.id, registryRecordData: applicationRecord }; + return { applicationRecordId: result.data.id, applicationRecordData: applicationRecord }; } async createApplicationDeploymentRequest (data: { @@ -96,8 +97,8 @@ export class Registry { repository: string, environmentVariables: { [key: string]: string } }): Promise<{ - registryRecordId: string, - registryRecordData: ApplicationDeploymentRequest + applicationDeploymentRequestId: string, + applicationDeploymentRequestData: ApplicationDeploymentRequest }> { const crn = this.getCrn(data.appName); const records = await this.registry.resolveNames([crn]); @@ -109,7 +110,7 @@ export class Registry { // Create record of type ApplicationDeploymentRequest and publish const applicationDeploymentRequest = { - type: DEPLOYMENT_RECORD_TYPE, + type: APP_DEPLOYMENT_REQUEST_TYPE, version: '1.0.0', name: `${applicationRecord.attributes.name}@${applicationRecord.attributes.app_version}`, application: `${crn}@${applicationRecord.attributes.app_version}`, @@ -141,7 +142,21 @@ export class Registry { log(`Application deployment request record published: ${result.data.id}`); log('Application deployment request data:', applicationDeploymentRequest); - return { registryRecordId: result.data.id, registryRecordData: applicationDeploymentRequest }; + return { applicationDeploymentRequestId: result.data.id, applicationDeploymentRequestData: applicationDeploymentRequest }; + } + + /** + * Fetch ApplicationDeploymentRecords for deployments + */ + async getDeploymentRecords (deployments: Deployment[]): Promise { + // Fetch ApplicationDeploymentRecords for corresponding ApplicationRecord set in deployments + // TODO: Implement Laconicd GQL query to filter records by multiple values for an attribute + const records = await this.registry.queryRecords({ + type: APP_DEPLOYMENT_RECORD_TYPE + }, true); + + // Filter records with ApplicationRecord ids + return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application)); } getCrn (packageJsonName: string): string { diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index 02602aae..f583bdab 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -30,7 +30,7 @@ export const createResolvers = async (service: Service): Promise => { }, deployments: async (_: any, { projectId }: { projectId: string }) => { - return service.getDeployementsByProjectId(projectId); + return service.getDeploymentsByProjectId(projectId); }, environmentVariables: async (_: any, { projectId }: { projectId: string }) => { diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index 8d048078..620dec72 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -91,7 +91,7 @@ type Deployment { branch: String! commitHash: String! commitMessage: String! - url: String! + url: String environment: Environment! isCurrent: Boolean! status: DeploymentStatus! diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index 8edb63a0..b2ba8fc1 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -14,14 +14,16 @@ import { Project } from './entity/Project'; import { Permission, ProjectMember } from './entity/ProjectMember'; import { User } from './entity/User'; import { Registry } from './registry'; -import { GitHubConfig } from './config'; -import { GitPushEventPayload } from './types'; +import { GitHubConfig, RegistryConfig } from './config'; +import { AppDeploymentRecord, GitPushEventPayload } from './types'; const log = debug('snowball:service'); + const GITHUB_UNIQUE_WEBHOOK_ERROR = 'Hook already exists on this repository'; interface Config { gitHubConfig: GitHubConfig + registryConfig: RegistryConfig } export class Service { @@ -30,11 +32,113 @@ export class Service { private registry: Registry; private config: Config; + private deployRecordCheckTimeout?: NodeJS.Timeout; + constructor (config: Config, db: Database, app: OAuthApp, registry: Registry) { this.db = db; this.oauthApp = app; this.registry = registry; this.config = config; + this.init(); + } + + /** + * Initialize services + */ + init (): void { + // Start check for ApplicationDeploymentRecords asynchronously + this.checkDeployRecordsAndUpdate(); + } + + /** + * Destroy services + */ + destroy (): void { + clearTimeout(this.deployRecordCheckTimeout); + } + + /** + * Checks for ApplicationDeploymentRecord and update corresponding deployments + * Continues check in loop after a delay of DEPLOY_RECORD_CHECK_DELAY_MS + */ + 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 + } + }); + + if (deployments.length) { + log(`Found ${deployments.length} deployments in ${DeploymentStatus.Building} state`); + + // Fetch ApplicationDeploymentRecord for deployments + const records = await this.registry.getDeploymentRecords(deployments); + log(`Found ${records.length} ApplicationDeploymentRecords`); + + // Update deployments for which ApplicationDeploymentRecords were returned + await this.updateDeploymentsWithRecordData(records); + } + + this.deployRecordCheckTimeout = setTimeout(() => { + this.checkDeployRecordsAndUpdate(); + }, this.config.registryConfig.fetchDeploymentRecordDelay); + } + + /** + * Update deployments with ApplicationDeploymentRecord data + */ + async updateDeploymentsWithRecordData (records: AppDeploymentRecord[]): Promise { + // Get deployments for ApplicationDeploymentRecords + const deployments = await this.db.getDeployments({ + where: records.map(record => ({ + applicationRecordId: record.attributes.application + })), + order: { + createdAt: 'DESC' + } + }); + + // Get project IDs of deployments that are in production environment + const productionDeploymentProjectIds = deployments.reduce((acc, deployment): Set => { + if (deployment.environment === Environment.Production) { + acc.add(deployment.projectId); + } + + return acc; + }, new Set()); + + // Set old deployments isCurrent to false + await Promise.all(Array.from(productionDeploymentProjectIds).map( + // TODO: Add DB method to update multiple deployments in single query + async (projectId) => this.db.updateDeployment({ projectId }, { isCurrent: false }) + )); + + const recordToDeploymentsMap = deployments.reduce((acc: {[key: string]: Deployment}, deployment) => { + acc[deployment.applicationRecordId] = deployment; + return acc; + }, {}); + + // Update deployment data for ApplicationDeploymentRecords + const deploymentUpdatePromises = records.map(async (record) => { + const deployment = recordToDeploymentsMap[record.attributes.application]; + + await this.db.updateDeploymentById( + deployment.id, + { + applicationDeploymentRecordId: record.id, + applicationDeploymentRecordData: record.attributes, + url: record.attributes.url, + status: DeploymentStatus.Ready, + isCurrent: deployment.environment === Environment.Production + } + ); + + log(`Updated deployment deployment ${deployment.id} with URL ${record.attributes.url}`); + }); + + await Promise.all(deploymentUpdatePromises); } async getUser (userId: string): Promise { @@ -67,7 +171,7 @@ export class Service { return dbProjects; } - async getDeployementsByProjectId (projectId: string): Promise { + async getDeploymentsByProjectId (projectId: string): Promise { const dbDeployments = await this.db.getDeploymentsByProjectId(projectId); return dbDeployments; } @@ -187,7 +291,7 @@ export class Service { const octokit = await this.getOctokit(userId); - const newDeployement = await this.createDeployment(userId, + const newDeployment = await this.createDeployment(userId, octokit, { project: oldDeployment.project, @@ -199,7 +303,7 @@ export class Service { commitMessage: oldDeployment.commitMessage }); - return newDeployement; + return newDeployment; } async createDeployment ( @@ -232,24 +336,13 @@ export class Service { } // TODO: Set environment variables for each deployment (environment variables can`t be set in application record) - const { registryRecordId, registryRecordData } = await this.registry.createApplicationRecord({ + const { applicationRecordId, applicationRecordData } = await this.registry.createApplicationRecord({ packageJSON, appType: data.project!.template!, commitHash: data.commitHash!, repoUrl: recordData.repoUrl }); - // Check if new deployment is set to current - if (data.isCurrent) { - // Update previous current deployment - await this.db.updateDeployment({ - project: { - id: data.project.id - }, - isCurrent: true - }, { isCurrent: false }); - } - // Update previous deployment with prod branch domain // TODO: Fix unique constraint error for domain await this.db.updateDeployment({ @@ -258,7 +351,7 @@ export class Service { domain: null }); - const newDeployement = await this.db.addDeployement({ + const newDeployment = await this.db.addDeployment({ project: data.project, branch: data.branch, commitHash: data.commitHash, @@ -266,16 +359,16 @@ export class Service { environment: data.environment, isCurrent: data.isCurrent, status: DeploymentStatus.Building, - registryRecordId, - registryRecordData, + applicationRecordId, + applicationRecordData, domain: data.domain, createdBy: Object.assign(new User(), { id: userId }) }); - log(`Created deployment ${newDeployement.id} and published application record ${registryRecordId}`); - return newDeployement; + log(`Created deployment ${newDeployment.id} and published application record ${applicationRecordId}`); + return newDeployment; } async addProject (userId: string, organizationSlug: string, data: DeepPartial): Promise { @@ -327,17 +420,17 @@ export class Service { return acc; }, {} as { [key: string]: string }); - const { registryRecordId, registryRecordData } = await this.registry.createApplicationDeploymentRequest( + const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest( { - appName: newDeployment.registryRecordData.name!, + appName: newDeployment.applicationRecordData.name!, commitHash: latestCommit.sha, repository: repoDetails.html_url, environmentVariables: environmentVariablesObj }); await this.db.updateProjectById(project.id, { - registryRecordId, - registryRecordData + applicationDeploymentRequestId, + applicationDeploymentRequestData }); await this.createRepoHook(octokit, project); @@ -444,7 +537,7 @@ export class Service { const octokit = await this.getOctokit(userId); - const newDeployement = await this.createDeployment(userId, + const newDeployment = await this.createDeployment(userId, octokit, { project: oldDeployment.project, @@ -457,7 +550,7 @@ export class Service { commitMessage: oldDeployment.commitMessage }); - return newDeployement; + return newDeployment; } async rollbackDeployment (projectId: string, deploymentId: string): Promise { @@ -475,7 +568,7 @@ export class Service { }); if (!oldCurrentDeployment) { - throw new Error('Current deployement doesnot exist'); + throw new Error('Current deployment doesnot exist'); } const oldCurrentDeploymentUpdate = await this.db.updateDeploymentById(oldCurrentDeployment.id, { isCurrent: false, domain: null }); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 9fb154cb..71626645 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -25,3 +25,27 @@ export interface GitPushEventPayload { message: string; }; } + +export interface AppDeploymentRecordAttributes { + application: string; + dns: string; + meta: string; + name: string; + request: string; + type: string; + url: string; + version: string; +} + +interface RegistryRecord { + id: string; + names: string[] | null; + owners: string[]; + bondId: string; + createTime: string; + expiryTime: string; +} + +export interface AppDeploymentRecord extends RegistryRecord { + attributes: AppDeploymentRecordAttributes +} diff --git a/packages/backend/test/fixtures/deployments.json b/packages/backend/test/fixtures/deployments.json index 25176713..5eb682a8 100644 --- a/packages/backend/test/fixtures/deployments.json +++ b/packages/backend/test/fixtures/deployments.json @@ -7,8 +7,8 @@ "status": "Ready", "environment": "Production", "isCurrent": true, - "registryRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "registryRecordData": {}, + "applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationRecordData": {}, "branch": "main", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -22,8 +22,8 @@ "status": "Ready", "environment": "Preview", "isCurrent": false, - "registryRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi", - "registryRecordData": {}, + "applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi", + "applicationRecordData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -37,8 +37,8 @@ "status": "Ready", "environment": "Development", "isCurrent": false, - "registryRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", - "registryRecordData": {}, + "applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", + "applicationRecordData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -52,8 +52,8 @@ "status": "Ready", "environment": "Production", "isCurrent": false, - "registryRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi", - "registryRecordData": {}, + "applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi", + "applicationRecordData": {}, "branch": "prod", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -67,8 +67,8 @@ "status": "Ready", "environment": "Production", "isCurrent": true, - "registryRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "registryRecordData": {}, + "applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationRecordData": {}, "branch": "main", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -82,8 +82,8 @@ "status": "Ready", "environment": "Preview", "isCurrent": false, - "registryRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", - "registryRecordData": {}, + "applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", + "applicationRecordData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -97,8 +97,8 @@ "status": "Ready", "environment": "Development", "isCurrent": false, - "registryRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi", - "registryRecordData": {}, + "applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi", + "applicationRecordData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -112,8 +112,8 @@ "status": "Ready", "environment": "Production", "isCurrent": true, - "registryRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "registryRecordData": {}, + "applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationRecordData": {}, "branch": "main", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -127,8 +127,8 @@ "status": "Ready", "environment": "Preview", "isCurrent": false, - "registryRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "registryRecordData": {}, + "applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationRecordData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -142,8 +142,8 @@ "status": "Ready", "environment": "Development", "isCurrent": false, - "registryRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", - "registryRecordData": {}, + "applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", + "applicationRecordData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -157,8 +157,8 @@ "status": "Ready", "environment": "Production", "isCurrent": true, - "registryRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", - "registryRecordData": {}, + "applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", + "applicationRecordData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", diff --git a/packages/backend/test/fixtures/projects.json b/packages/backend/test/fixtures/projects.json index f2def3f2..55c5ee23 100644 --- a/packages/backend/test/fixtures/projects.json +++ b/packages/backend/test/fixtures/projects.json @@ -10,8 +10,8 @@ "framework": "test", "webhooks": [], "icon": "", - "registryRecordId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "registryRecordData": {}, + "applicationDeploymentRequestId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "subDomain": "testProject.snowball.xyz" }, { @@ -25,8 +25,8 @@ "framework": "test-2", "webhooks": [], "icon": "", - "registryRecordId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "registryRecordData": {}, + "applicationDeploymentRequestId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "subDomain": "testProject-2.snowball.xyz" }, { @@ -40,8 +40,8 @@ "framework": "test-3", "webhooks": [], "icon": "", - "registryRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "registryRecordData": {}, + "applicationDeploymentRequestId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "subDomain": "iglootools.snowball.xyz" }, { @@ -55,8 +55,8 @@ "framework": "test-4", "webhooks": [], "icon": "", - "registryRecordId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "registryRecordData": {}, + "applicationDeploymentRequestId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "subDomain": "iglootools-2.snowball.xyz" }, { @@ -70,8 +70,8 @@ "framework": "test-5", "webhooks": [], "icon": "", - "registryRecordId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "registryRecordData": {}, + "applicationDeploymentRequestId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "subDomain": "snowball-2.snowball.xyz" } ] diff --git a/packages/backend/test/publish-deploy-records.ts b/packages/backend/test/publish-deploy-records.ts index 6f2d26b0..f5636c4e 100644 --- a/packages/backend/test/publish-deploy-records.ts +++ b/packages/backend/test/publish-deploy-records.ts @@ -41,8 +41,8 @@ async function main () { const applicationDeploymentRecord = { type: 'ApplicationDeploymentRecord', version: '0.0.1', - name: deployment.registryRecordData.name, - application: deployment.registryRecordId, + name: deployment.applicationRecordData.name, + application: deployment.applicationRecordId, // TODO: Create DNS record dns: 'bafyreihlymqggsgqiqawvehkpr2imt4l3u6q7um7xzjrux5rhsvwnuyewm', @@ -53,7 +53,7 @@ async function main () { so: '66fcfa49a1664d4cb4ce4f72c1c0e151' }), - request: deployment.project.registryRecordId, + request: deployment.project.applicationDeploymentRequestId, url }; diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx index 5add83f9..1121eb54 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx @@ -90,7 +90,9 @@ const DeploymentDetailsCard = ({
- {deployment.url} + {deployment.url && ( + {deployment.url} + )} - ^ Visit + ^ Visit setAssignDomainDialog(!assignDomainDialog)} diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentDialogBodyCard.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentDialogBodyCard.tsx index 65a5a4b6..e4388bf8 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentDialogBodyCard.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentDialogBodyCard.tsx @@ -28,9 +28,11 @@ const DeploymentDialogBodyCard = ({ color={chip.color} /> )} - - {deployment.url} - + {deployment.url && ( + + {deployment.url} + + )} ^ {deployment.branch} ^{' '} {deployment.commitHash.substring(0, SHORT_COMMIT_HASH_LENGTH)}{' '} diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index e75d0aba..71c44c00 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -63,7 +63,7 @@ export type Deployment = { branch: string commitHash: string commitMessage: string - url: string + url?: string environment: Environment isCurrent: boolean status: DeploymentStatus From c3d1b4f3eb226f475a4bdaebfd156e85b0672c51 Mon Sep 17 00:00:00 2001 From: Nabarun Gogoi Date: Mon, 19 Feb 2024 16:55:07 +0530 Subject: [PATCH 03/12] Use branches from GitHub API in edit domain dialog (#74) * Use branches from GitHub API * Disable git branch input if repo not found * Disable git branch if branches is empty * Use async select for accounts dropdown * Log actual HTTP error in console --------- Co-authored-by: neeraj --- .../projects/create/RepositoryList.tsx | 8 ++--- .../projects/project/settings/DomainCard.tsx | 7 ++-- .../project/settings/EditDomainDialog.tsx | 15 ++++---- .../frontend/src/context/OctokitContext.tsx | 4 +-- .../pages/org-slug/projects/create/index.tsx | 2 +- .../pages/org-slug/projects/id/Overview.tsx | 4 --- .../org-slug/projects/id/settings/Domains.tsx | 34 +++++++++++++++++-- packages/frontend/src/types.ts | 9 ----- 8 files changed, 50 insertions(+), 33 deletions(-) diff --git a/packages/frontend/src/components/projects/create/RepositoryList.tsx b/packages/frontend/src/components/projects/create/RepositoryList.tsx index ad69e776..d0f360bb 100644 --- a/packages/frontend/src/components/projects/create/RepositoryList.tsx +++ b/packages/frontend/src/components/projects/create/RepositoryList.tsx @@ -3,11 +3,12 @@ import { Octokit } from 'octokit'; import assert from 'assert'; import { useDebounce } from 'usehooks-ts'; -import { Button, Typography, Option, Select } from '@material-tailwind/react'; +import { Button, Typography, Option } from '@material-tailwind/react'; import SearchBar from '../../SearchBar'; import ProjectRepoCard from './ProjectRepoCard'; import { GitOrgDetails, GitRepositoryDetails } from '../../../types'; +import AsyncSelect from '../../shared/AsyncSelect'; const DEFAULT_SEARCHED_REPO = ''; const REPOS_PER_PAGE = 5; @@ -109,8 +110,7 @@ const RepositoryList = ({ octokit }: RepositoryListProps) => {
- {/* TODO: Fix selection of Git user at start */} - +
Promise; } @@ -44,7 +43,7 @@ const DOMAIN_RECORD = { const DomainCard = ({ domains, domain, - repo, + branches, project, onUpdate, }: DomainCardProps) => { @@ -188,7 +187,7 @@ const DomainCard = ({ domains={domains} open={editDialogOpen} domain={domain} - repo={repo} + branches={branches} onUpdate={onUpdate} /> diff --git a/packages/frontend/src/components/projects/project/settings/EditDomainDialog.tsx b/packages/frontend/src/components/projects/project/settings/EditDomainDialog.tsx index 78e4aceb..b9d2866c 100644 --- a/packages/frontend/src/components/projects/project/settings/EditDomainDialog.tsx +++ b/packages/frontend/src/components/projects/project/settings/EditDomainDialog.tsx @@ -15,7 +15,6 @@ import { Option, } from '@material-tailwind/react'; -import { RepositoryDetails } from '../../../../types'; import { useGQLClient } from '../../../../context/GQLClientContext'; const DEFAULT_REDIRECT_OPTIONS = ['none']; @@ -25,7 +24,7 @@ interface EditDomainDialogProp { open: boolean; handleOpen: () => void; domain: Domain; - repo: RepositoryDetails; + branches: string[]; onUpdate: () => Promise; } @@ -40,7 +39,7 @@ const EditDomainDialog = ({ open, handleOpen, domain, - repo, + branches, onUpdate, }: EditDomainDialogProp) => { const client = useGQLClient(); @@ -120,7 +119,7 @@ const EditDomainDialog = ({ branch: domain.branch, redirectedTo: getRedirectUrl(domain), }); - }, [domain, repo]); + }, [domain]); return ( @@ -166,9 +165,13 @@ const EditDomainDialog = ({ repo.branch.includes(value), + validate: (value) => + Boolean(branches.length) ? branches.includes(value) : true, })} - disabled={watch('redirectedTo') !== DEFAULT_REDIRECT_OPTIONS[0]} + disabled={ + !Boolean(branches.length) || + watch('redirectedTo') !== DEFAULT_REDIRECT_OPTIONS[0] + } /> {!isValid && ( diff --git a/packages/frontend/src/context/OctokitContext.tsx b/packages/frontend/src/context/OctokitContext.tsx index 0eed37ce..21ffe246 100644 --- a/packages/frontend/src/context/OctokitContext.tsx +++ b/packages/frontend/src/context/OctokitContext.tsx @@ -14,13 +14,13 @@ import { useGQLClient } from './GQLClientContext'; const UNAUTHORIZED_ERROR_CODE = 401; interface ContextValue { - octokit: Octokit | null; + octokit: Octokit; isAuth: boolean; updateAuth: () => void; } const OctokitContext = createContext({ - octokit: null, + octokit: new Octokit(), isAuth: false, updateAuth: () => {}, }); diff --git a/packages/frontend/src/pages/org-slug/projects/create/index.tsx b/packages/frontend/src/pages/org-slug/projects/create/index.tsx index 8658d18c..a48f4dd4 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/index.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/index.tsx @@ -24,7 +24,7 @@ const NewProject = () => { })}
Import a repository
- + ) : ( diff --git a/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx b/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx index c5db5c34..a5f1ad73 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx @@ -24,10 +24,6 @@ const OverviewTabPanel = () => { const { project } = useOutletContext(); useEffect(() => { - if (!octokit) { - return; - } - // TODO: Save repo commits in DB and avoid using GitHub API in frontend // TODO: Figure out fetching latest commits for all branches const fetchRepoActivity = async () => { diff --git a/packages/frontend/src/pages/org-slug/projects/id/settings/Domains.tsx b/packages/frontend/src/pages/org-slug/projects/id/settings/Domains.tsx index f912e8fa..24457b1a 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/settings/Domains.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/settings/Domains.tsx @@ -1,4 +1,5 @@ -import React, { useEffect, useState } from 'react'; +import { RequestError } from 'octokit'; +import React, { useCallback, useEffect, useState } from 'react'; import { Link, useOutletContext } from 'react-router-dom'; import { Domain } from 'gql-client'; @@ -6,14 +7,41 @@ import { Button, Typography } from '@material-tailwind/react'; import DomainCard from '../../../../../components/projects/project/settings/DomainCard'; import { useGQLClient } from '../../../../../context/GQLClientContext'; -import repositories from '../../../../../assets/repositories.json'; import { OutletContextType } from '../../../../../types'; +import { useOctokit } from '../../../../../context/OctokitContext'; const Domains = () => { const client = useGQLClient(); + const { octokit } = useOctokit(); const { project } = useOutletContext(); const [domains, setDomains] = useState([]); + const [branches, setBranches] = useState([]); + + const fetchBranches = useCallback(async () => { + const [owner, repo] = project.repository.split('/'); + + try { + const result = await octokit.rest.repos.listBranches({ + owner, + repo, + }); + + const branches = result.data.map((repo) => repo.name); + + setBranches(branches); + } catch (err) { + if (!(err instanceof RequestError && err.status === 404)) { + throw err; + } + + console.error(err); + } + }, []); + + useEffect(() => { + fetchBranches(); + }, []); const fetchDomains = async () => { if (project === undefined) { @@ -46,7 +74,7 @@ const Domains = () => { domain={domain} key={domain.id} // TODO: Use github API for getting linked repository - repo={repositories[0]!} + branches={branches} project={project} onUpdate={fetchDomains} /> diff --git a/packages/frontend/src/types.ts b/packages/frontend/src/types.ts index 1ece61d3..2ca22da6 100644 --- a/packages/frontend/src/types.ts +++ b/packages/frontend/src/types.ts @@ -6,15 +6,6 @@ export interface GitOrgDetails { avatar_url: string; } -// TODO: Use GitRepositoryDetails -export interface RepositoryDetails { - title: string; - updatedAt: string; - user: string; - private: boolean; - branch: string[]; -} - export interface GitRepositoryDetails { id: number; name: string; From a45fb4c6175ca9dcc94bad23035c912b1f18f9f1 Mon Sep 17 00:00:00 2001 From: Nabarun Gogoi Date: Tue, 20 Feb 2024 10:53:42 +0530 Subject: [PATCH 04/12] Fix deployments visit URL and date filter (#78) * Add method to update multiple deployments in single query * Fix deployments URL visit and date filter * Clean fixtures data * Update fixtures data --------- Co-authored-by: neeraj --- packages/backend/src/database.ts | 13 ++ packages/backend/src/service.ts | 16 +- .../backend/test/fixtures/organizations.json | 4 +- .../test/fixtures/primary-domains.json | 12 +- .../test/fixtures/redirected-domains.json | 6 +- ...nizations.json => user-organizations.json} | 0 packages/backend/test/fixtures/users.json | 12 +- packages/backend/test/initialize-db.ts | 2 +- packages/frontend/src/assets/domains.json | 6 +- packages/frontend/src/assets/members.json | 17 -- .../frontend/src/assets/process-logs.json | 2 +- packages/frontend/src/assets/projects.json | 190 ------------------ .../deployments/DeploymentDetailsCard.tsx | 6 +- .../org-slug/projects/id/Deployments.tsx | 6 +- 14 files changed, 45 insertions(+), 247 deletions(-) rename packages/backend/test/fixtures/{user-orgnizations.json => user-organizations.json} (100%) delete mode 100644 packages/frontend/src/assets/members.json delete mode 100644 packages/frontend/src/assets/projects.json diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index 7c76f928..a45c354d 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -316,6 +316,19 @@ export class Database { return Boolean(updateResult.affected); } + async updateDeploymentsByProjectIds (projectIds: string[], data: DeepPartial): Promise { + const deploymentRepository = this.dataSource.getRepository(Deployment); + + const updateResult = await deploymentRepository + .createQueryBuilder() + .update(Deployment) + .set(data) + .where('projectId IN (:...projectIds)', { projectIds }) + .execute(); + + return Boolean(updateResult.affected); + } + async addProject (userId: string, organizationId: string, data: DeepPartial): Promise { const projectRepository = this.dataSource.getRepository(Project); diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index b2ba8fc1..0ce326ff 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -78,7 +78,9 @@ export class Service { log(`Found ${records.length} ApplicationDeploymentRecords`); // Update deployments for which ApplicationDeploymentRecords were returned - await this.updateDeploymentsWithRecordData(records); + if (records.length) { + await this.updateDeploymentsWithRecordData(records); + } } this.deployRecordCheckTimeout = setTimeout(() => { @@ -110,10 +112,7 @@ export class Service { }, new Set()); // Set old deployments isCurrent to false - await Promise.all(Array.from(productionDeploymentProjectIds).map( - // TODO: Add DB method to update multiple deployments in single query - async (projectId) => this.db.updateDeployment({ projectId }, { isCurrent: false }) - )); + await this.db.updateDeploymentsByProjectIds(Array.from(productionDeploymentProjectIds), { isCurrent: false }); const recordToDeploymentsMap = deployments.reduce((acc: {[key: string]: Deployment}, deployment) => { acc[deployment.applicationRecordId] = deployment; @@ -135,7 +134,7 @@ export class Service { } ); - log(`Updated deployment deployment ${deployment.id} with URL ${record.attributes.url}`); + log(`Updated deployment ${deployment.id} with URL ${record.attributes.url}`); }); await Promise.all(deploymentUpdatePromises); @@ -295,7 +294,6 @@ export class Service { octokit, { project: oldDeployment.project, - isCurrent: true, branch: oldDeployment.branch, environment: Environment.Production, domain: prodBranchDomains[0], @@ -357,7 +355,6 @@ export class Service { commitHash: data.commitHash, commitMessage: data.commitMessage, environment: data.environment, - isCurrent: data.isCurrent, status: DeploymentStatus.Building, applicationRecordId, applicationRecordData, @@ -400,7 +397,6 @@ export class Service { octokit, { project, - isCurrent: true, branch: project.prodBranch, environment: Environment.Production, domain: null, @@ -486,7 +482,6 @@ export class Service { octokit, { project, - isCurrent: project.prodBranch === branch, branch, environment: project.prodBranch === branch ? Environment.Production : Environment.Preview, domain, @@ -543,7 +538,6 @@ export class Service { project: oldDeployment.project, // TODO: Put isCurrent field in project branch: oldDeployment.branch, - isCurrent: true, environment: Environment.Production, domain: oldDeployment.domain, commitHash: oldDeployment.commitHash, diff --git a/packages/backend/test/fixtures/organizations.json b/packages/backend/test/fixtures/organizations.json index bf934e81..ace516d3 100644 --- a/packages/backend/test/fixtures/organizations.json +++ b/packages/backend/test/fixtures/organizations.json @@ -6,7 +6,7 @@ }, { "id": "7eb9b3eb-eb74-4b53-b59a-69884c82a7fb", - "name": "AirFoil", - "slug": "airfoil-2" + "name": "Laconic", + "slug": "laconic-2" } ] diff --git a/packages/backend/test/fixtures/primary-domains.json b/packages/backend/test/fixtures/primary-domains.json index b1cb0a99..23e2666d 100644 --- a/packages/backend/test/fixtures/primary-domains.json +++ b/packages/backend/test/fixtures/primary-domains.json @@ -1,37 +1,37 @@ [ { "projectIndex": 0, - "name": "randomurl.snowballtools.xyz", + "name": "example.snowballtools.xyz", "status": "Live", "branch": "main" }, { "projectIndex": 0, - "name": "saugatt.com", + "name": "example.org", "status": "Pending", "branch": "test" }, { "projectIndex": 1, - "name": "randomurl.snowballtools.xyz", + "name": "example.snowballtools.xyz", "status": "Live", "branch": "main" }, { "projectIndex": 1, - "name": "saugatt.com", + "name": "example.org", "status": "Pending", "branch": "test" }, { "projectIndex": 2, - "name": "randomurl.snowballtools.xyz", + "name": "example.snowballtools.xyz", "status": "Live", "branch": "main" }, { "projectIndex": 2, - "name": "saugatt.com", + "name": "example.org", "status": "Pending", "branch": "test" }, diff --git a/packages/backend/test/fixtures/redirected-domains.json b/packages/backend/test/fixtures/redirected-domains.json index d526bde3..64fa9736 100644 --- a/packages/backend/test/fixtures/redirected-domains.json +++ b/packages/backend/test/fixtures/redirected-domains.json @@ -1,21 +1,21 @@ [ { "projectIndex": 0, - "name": "www.saugatt.com", + "name": "www.example.org", "status": "Pending", "redirectToIndex": 1, "branch": "test" }, { "projectIndex": 1, - "name": "www.saugatt.com", + "name": "www.example.org", "status": "Pending", "redirectToIndex": 3, "branch": "test" }, { "projectIndex": 2, - "name": "www.saugatt.com", + "name": "www.example.org", "status": "Pending", "redirectToIndex": 5, "branch": "test" diff --git a/packages/backend/test/fixtures/user-orgnizations.json b/packages/backend/test/fixtures/user-organizations.json similarity index 100% rename from packages/backend/test/fixtures/user-orgnizations.json rename to packages/backend/test/fixtures/user-organizations.json diff --git a/packages/backend/test/fixtures/users.json b/packages/backend/test/fixtures/users.json index aacd7b6b..118beba5 100644 --- a/packages/backend/test/fixtures/users.json +++ b/packages/backend/test/fixtures/users.json @@ -1,20 +1,20 @@ [ { "id": "59f4355d-9549-4aac-9b54-eeefceeabef0", - "name": "Saugat Yadav", - "email": "saugaty@airfoil.studio", + "name": "Snowball", + "email": "snowball@snowballtools.xyz", "isVerified": true }, { "id": "e505b212-8da6-48b2-9614-098225dab34b", - "name": "Gideon Low", - "email": "gideonl@airfoil.studio", + "name": "Alice Anderson", + "email": "alice@snowballtools.xyz", "isVerified": true }, { "id": "cd892fad-9138-4aa2-a62c-414a32776ea7", - "name": "Sushan Yadav", - "email": "sushany@airfoil.studio", + "name": "Bob Banner", + "email": "bob@snowballtools.xyz", "isVerified": true } ] diff --git a/packages/backend/test/initialize-db.ts b/packages/backend/test/initialize-db.ts index fdfeeedb..0aade587 100644 --- a/packages/backend/test/initialize-db.ts +++ b/packages/backend/test/initialize-db.ts @@ -20,7 +20,7 @@ const log = debug('snowball:initialize-database'); const USER_DATA_PATH = './fixtures/users.json'; const PROJECT_DATA_PATH = './fixtures/projects.json'; const ORGANIZATION_DATA_PATH = './fixtures/organizations.json'; -const USER_ORGANIZATION_DATA_PATH = './fixtures/user-orgnizations.json'; +const USER_ORGANIZATION_DATA_PATH = './fixtures/user-organizations.json'; const PROJECT_MEMBER_DATA_PATH = './fixtures/project-members.json'; const PRIMARY_DOMAIN_DATA_PATH = './fixtures/primary-domains.json'; const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json'; diff --git a/packages/frontend/src/assets/domains.json b/packages/frontend/src/assets/domains.json index e17d1738..11020b61 100644 --- a/packages/frontend/src/assets/domains.json +++ b/packages/frontend/src/assets/domains.json @@ -2,7 +2,7 @@ { "id": 1, "projectid": 1, - "name": "randomurl.snowballtools.xyz", + "name": "example.snowballtools.xyz", "status": "live", "record": null, "isRedirectedto": false @@ -10,7 +10,7 @@ { "id": 2, "projectid": 1, - "name": "saugatt.com", + "name": "example.org", "status": "pending", "record": { "type": "A", @@ -22,7 +22,7 @@ { "id": 3, "projectid": 1, - "name": "www.saugatt.com", + "name": "www.example.org", "status": "pending", "record": { "type": "CNAME", diff --git a/packages/frontend/src/assets/members.json b/packages/frontend/src/assets/members.json deleted file mode 100644 index 6e38602c..00000000 --- a/packages/frontend/src/assets/members.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - { - "name": "Saugat Yadav", - "email": "saugaty@airfoil.studio", - "id": 1 - }, - { - "name": "Gideon Low", - "email": "gideonl@airfoil.studio", - "id": 2 - }, - { - "name": "Sushan Yadav", - "email": "sushany@airfoil.studio", - "id": 3 - } -] diff --git a/packages/frontend/src/assets/process-logs.json b/packages/frontend/src/assets/process-logs.json index 34c7e24b..097a0fa9 100644 --- a/packages/frontend/src/assets/process-logs.json +++ b/packages/frontend/src/assets/process-logs.json @@ -1,6 +1,6 @@ [ "[20:50:03.502] Running build in Washington, D.C., USA (East) – iad1", - "[20:50:03.641] Cloning github.com/saugatyadav11/nextjs2 (Branch: main, Commit: 4a5f47f)", + "[20:50:03.641] Cloning github.com/cerc-io/nextjs2 (Branch: main, Commit: 4a5f47f)", "[20:50:04.004] Previous build cache not available", "[20:50:04.118] Cloning completed: 480.574ms", "[20:50:04.382] Running 'vercel build'", diff --git a/packages/frontend/src/assets/projects.json b/packages/frontend/src/assets/projects.json deleted file mode 100644 index a344fdf7..00000000 --- a/packages/frontend/src/assets/projects.json +++ /dev/null @@ -1,190 +0,0 @@ -[ - { - "id": 1, - "icon": "^", - "name": "iglootools", - "title": "Iglootools", - "domain": null, - "organization": "Airfoil", - "url": "iglootools.co", - "createdAt": "2023-12-07T04:20:00", - "createdBy": "Alice", - "deployment": "iglootools.snowballtools.co", - "source": "feature/add-remote-control", - "latestCommit": { - "message": "subscription added", - "createdAt": "2023-12-11T04:20:00", - "branch": "main" - }, - "repositoryId": 1, - "members": [ - { - "id": 1, - "permissions": [] - }, - { - "id": 2, - "permissions": ["view", "edit"] - }, - { - "id": 3, - "permissions": ["view"] - } - ], - "ownerId": 1 - }, - { - "id": 2, - "icon": "^", - "name": "snowball-starter-kit", - "title": "Snowball Starter Kit", - "domain": null, - "organization": "Snowball", - "url": "starterkit.snowballtools.com", - "createdAt": "2023-12-04T04:20:00", - "createdBy": "Bob", - "deployment": "deploy.snowballtools.com", - "source": "prod/add-docker-compose", - "latestCommit": { - "message": "component updates", - "createdAt": "2023-12-11T04:20:00", - "branch": "staging" - }, - "repositoryId": 1, - "members": [ - { - "id": 2, - "permissions": [] - }, - { - "id": 3, - "permissions": ["view"] - } - ], - "ownerId": 2 - }, - { - "id": 3, - "icon": "^", - "name": "web3-android", - "title": "Web3 Android", - "domain": null, - "organization": "Personal", - "url": "web3fordroids.com", - "createdAt": "2023-12-01T04:20:00", - "createdBy": "Charlie", - "deployment": "deploy.web3fordroids.com", - "source": "dev/style-page", - "latestCommit": { - "message": "No repo connected", - "createdAt": "2023-12-01T04:20:00", - "branch": "main" - }, - "repositoryId": 1, - "members": [ - { - "id": 1, - "permissions": [] - }, - { - "id": 2, - "permissions": ["view", "edit"] - }, - { - "id": 3, - "permissions": ["view"] - } - ], - "ownerId": 1 - }, - { - "id": 4, - "icon": "^", - "name": "passkeys-demo", - "title": "Passkeys Demo", - "domain": null, - "organization": "Airfoil", - "url": "passkeys.iglootools.xyz", - "createdAt": "2023-12-01T04:20:00", - "createdBy": "David", - "deployment": "demo.passkeys.xyz", - "source": "dev/style-page", - "latestCommit": { - "message": "hello world", - "createdAt": "2023-12-01T04:20:00", - "branch": "main" - }, - "repositoryId": 1, - "members": [ - { - "id": 1, - "permissions": [] - }, - { - "id": 2, - "permissions": ["view", "edit"] - }, - { - "id": 3, - "permissions": ["view"] - } - ], - "ownerId": 1 - }, - { - "id": 5, - "icon": "^", - "name": "iglootools", - "title": "Iglootools", - "domain": null, - "organization": "Airfoil", - "url": "iglootools.xyz", - "createdAt": "2023-12-11T04:20:00", - "createdBy": "Erin", - "deployment": "staging.snowballtools.com", - "source": "prod/fix-error", - "latestCommit": { - "message": "404 added", - "createdAt": "2023-12-09T04:20:00", - "branch": "main" - }, - "repositoryId": 1, - "members": [ - { - "id": 3, - "permissions": [] - } - ], - "ownerId": 3 - }, - { - "id": 6, - "icon": "^", - "name": "iglootools", - "title": "Iglootools", - "domain": null, - "organization": "Airfoil", - "url": "iglootools.xyz", - "createdAt": "2023-12-11T04:20:00", - "createdBy": "Frank", - "deployment": "iglootools.snowballtools.com", - "source": "prod/fix-error", - "latestCommit": { - "message": "design system integrated", - "createdAt": "2023-12-09T04:20:00", - "branch": "prod" - }, - "repositoryId": 1, - "members": [ - { - "id": 2, - "permissions": [] - }, - { - "id": 3, - "permissions": ["view"] - } - ], - "ownerId": 2 - } -] diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx index 1121eb54..60bc1237 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx @@ -122,11 +122,7 @@ const DeploymentDetailsCard = ({ - + ^ Visit { const dateMatch = !filterValue.updateAtRange || - (new Date(deployment.updatedAt) >= filterValue.updateAtRange!.from! && - new Date(deployment.updatedAt) <= filterValue.updateAtRange!.to!); + (new Date(Number(deployment.createdAt)) >= + filterValue.updateAtRange!.from! && + new Date(Number(deployment.createdAt)) <= + filterValue.updateAtRange!.to!); return branchMatch && statusMatch && dateMatch; }); From fc240c93d814cba707149f3a13cf197e61aaa373 Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Wed, 21 Feb 2024 00:05:45 -0600 Subject: [PATCH 05/12] SO deployable config for the backend. (#83) * webapp-ify * SO deployable config for the backend. * Use process.env * Detangle --- packages/backend/src/constants.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/constants.ts b/packages/backend/src/constants.ts index 7803a7b1..ea3b4610 100644 --- a/packages/backend/src/constants.ts +++ b/packages/backend/src/constants.ts @@ -1,8 +1,10 @@ -export const DEFAULT_CONFIG_FILE_PATH = 'environments/local.toml'; +import process from 'process'; + +export const DEFAULT_CONFIG_FILE_PATH = process.env.SNOWBALL_BACKEND_CONFIG_FILE_PATH || 'environments/local.toml'; export const DEFAULT_GQL_PATH = '/graphql'; // Note: temporary hardcoded user, later to be derived from auth token -export const USER_ID = '59f4355d-9549-4aac-9b54-eeefceeabef0'; +export const USER_ID = process.env.SNOWBALL_BACKEND_USER_ID || '60f4355d-9549-4aac-9b54-eeefceeabef0'; -export const PROJECT_DOMAIN = 'snowball.xyz'; +export const PROJECT_DOMAIN = process.env.SNOWBALL_BACKEND_PROJECT_DOMAIN || 'snowball.xyz'; From 8ca55cd88805f77b5341cac4356e74d25059bd08 Mon Sep 17 00:00:00 2001 From: Nabarun Gogoi Date: Wed, 21 Feb 2024 15:34:33 +0530 Subject: [PATCH 06/12] Implement polling for project deployment updates (#87) * Populate organization and user if db is empty * Use hardcoded user id from fixtures * Implement polling for get deployments query * Handle review changes --------- Co-authored-by: neeraj --- packages/backend/src/constants.ts | 2 +- packages/backend/src/database.ts | 32 +++++++++ packages/backend/src/utils.ts | 42 +++++++++++ packages/backend/test/initialize-db.ts | 70 ++++++------------- .../org-slug/projects/id/Deployments.tsx | 19 +++-- 5 files changed, 110 insertions(+), 55 deletions(-) diff --git a/packages/backend/src/constants.ts b/packages/backend/src/constants.ts index ea3b4610..bd3badad 100644 --- a/packages/backend/src/constants.ts +++ b/packages/backend/src/constants.ts @@ -5,6 +5,6 @@ export const DEFAULT_CONFIG_FILE_PATH = process.env.SNOWBALL_BACKEND_CONFIG_FILE export const DEFAULT_GQL_PATH = '/graphql'; // Note: temporary hardcoded user, later to be derived from auth token -export const USER_ID = process.env.SNOWBALL_BACKEND_USER_ID || '60f4355d-9549-4aac-9b54-eeefceeabef0'; +export const USER_ID = process.env.SNOWBALL_BACKEND_USER_ID || '59f4355d-9549-4aac-9b54-eeefceeabef0'; export const PROJECT_DOMAIN = process.env.SNOWBALL_BACKEND_PROJECT_DOMAIN || 'snowball.xyz'; diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index a45c354d..aa0e997f 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -14,6 +14,12 @@ import { ProjectMember } from './entity/ProjectMember'; import { EnvironmentVariable } from './entity/EnvironmentVariable'; import { Domain } from './entity/Domain'; import { PROJECT_DOMAIN } from './constants'; +import { getEntities, loadAndSaveData } from './utils'; +import { UserOrganization } from './entity/UserOrganization'; + +const ORGANIZATION_DATA_PATH = '../test/fixtures/organizations.json'; +const USER_DATA_PATH = '../test/fixtures/users.json'; +const USER_ORGANIZATION_DATA_PATH = '../test/fixtures/user-organizations.json'; const log = debug('snowball:database'); @@ -36,6 +42,25 @@ export class Database { async init (): Promise { await this.dataSource.initialize(); log('database initialized'); + + const organizations = await this.getOrganizations({}); + + if (!organizations.length) { + const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH)); + const savedOrgs = await loadAndSaveData(Organization, this.dataSource, [orgEntities[0]]); + + // TODO: Remove user once authenticated + const userEntities = await getEntities(path.resolve(__dirname, USER_DATA_PATH)); + const savedUsers = await loadAndSaveData(User, this.dataSource, [userEntities[0]]); + + const userOrganizationRelations = { + member: savedUsers, + organization: savedOrgs + }; + + const userOrgEntities = await getEntities(path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)); + await loadAndSaveData(UserOrganization, this.dataSource, [userOrgEntities[0]], userOrganizationRelations); + } } async getUser (options: FindOneOptions): Promise { @@ -60,6 +85,13 @@ export class Database { return updateResult.affected > 0; } + async getOrganizations (options: FindManyOptions): Promise { + const organizationRepository = this.dataSource.getRepository(Organization); + const organizations = await organizationRepository.find(options); + + return organizations; + } + async getOrganization (options: FindOneOptions): Promise { const organizationRepository = this.dataSource.getRepository(Organization); const organization = await organizationRepository.findOne(options); diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index f33963f9..f9d21e7a 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -2,6 +2,7 @@ import fs from 'fs-extra'; import path from 'path'; import toml from 'toml'; import debug from 'debug'; +import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm'; const log = debug('snowball:utils'); @@ -19,3 +20,44 @@ export const getConfig = async ( return config; }; + +export const checkFileExists = async (filePath: string): Promise => { + try { + await fs.access(filePath, fs.constants.F_OK); + return true; + } catch (err) { + log(err); + return false; + } +}; + +export const getEntities = async (filePath: string): Promise => { + const entitiesData = await fs.readFile(filePath, 'utf-8'); + const entities = JSON.parse(entitiesData); + return entities; +}; + +export const loadAndSaveData = async (entityType: EntityTarget, dataSource: DataSource, entities: any, relations?: any | undefined): Promise => { + const entityRepository = dataSource.getRepository(entityType); + + const savedEntity:Entity[] = []; + + for (const entityData of entities) { + let entity = entityRepository.create(entityData as DeepPartial); + + if (relations) { + for (const field in relations) { + const valueIndex = String(field + 'Index'); + + entity = { + ...entity, + [field]: relations[field][entityData[valueIndex]] + }; + } + } + const dbEntity = await entityRepository.save(entity); + savedEntity.push(dbEntity); + } + + return savedEntity; +}; diff --git a/packages/backend/test/initialize-db.ts b/packages/backend/test/initialize-db.ts index 0aade587..420ff03d 100644 --- a/packages/backend/test/initialize-db.ts +++ b/packages/backend/test/initialize-db.ts @@ -1,5 +1,4 @@ -import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm'; -import * as fs from 'fs/promises'; +import { DataSource } from 'typeorm'; import debug from 'debug'; import path from 'path'; @@ -11,7 +10,7 @@ import { EnvironmentVariable } from '../src/entity/EnvironmentVariable'; import { Domain } from '../src/entity/Domain'; import { ProjectMember } from '../src/entity/ProjectMember'; import { Deployment } from '../src/entity/Deployment'; -import { getConfig } from '../src/utils'; +import { checkFileExists, getConfig, getEntities, loadAndSaveData } from '../src/utils'; import { Config } from '../src/config'; import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants'; @@ -27,43 +26,20 @@ const DEPLOYMENT_DATA_PATH = './fixtures/deployments.json'; const ENVIRONMENT_VARIABLE_DATA_PATH = './fixtures/environment-variables.json'; const REDIRECTED_DOMAIN_DATA_PATH = './fixtures/redirected-domains.json'; -const loadAndSaveData = async (entityType: EntityTarget, dataSource: DataSource, filePath: string, relations?: any | undefined) => { - const entitiesData = await fs.readFile(filePath, 'utf-8'); - const entities = JSON.parse(entitiesData); - const entityRepository = dataSource.getRepository(entityType); - - const savedEntity:Entity[] = []; - - for (const entityData of entities) { - let entity = entityRepository.create(entityData as DeepPartial); - - if (relations) { - for (const field in relations) { - const valueIndex = String(field + 'Index'); - - entity = { - ...entity, - [field]: relations[field][entityData[valueIndex]] - }; - } - } - const dbEntity = await entityRepository.save(entity); - savedEntity.push(dbEntity); - } - - return savedEntity; -}; - const generateTestData = async (dataSource: DataSource) => { - const savedUsers = await loadAndSaveData(User, dataSource, path.resolve(__dirname, USER_DATA_PATH)); - const savedOrgs = await loadAndSaveData(Organization, dataSource, path.resolve(__dirname, ORGANIZATION_DATA_PATH)); + const userEntities = await getEntities(path.resolve(__dirname, USER_DATA_PATH)); + const savedUsers = await loadAndSaveData(User, dataSource, userEntities); + + const orgEntities = await getEntities(path.resolve(__dirname, ORGANIZATION_DATA_PATH)); + const savedOrgs = await loadAndSaveData(Organization, dataSource, orgEntities); const projectRelations = { owner: savedUsers, organization: savedOrgs }; - const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations); + const projectEntities = await getEntities(path.resolve(__dirname, PROJECT_DATA_PATH)); + const savedProjects = await loadAndSaveData(Project, dataSource, projectEntities, projectRelations); const domainRepository = dataSource.getRepository(Domain); @@ -71,14 +47,16 @@ const generateTestData = async (dataSource: DataSource) => { project: savedProjects }; - const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH), domainPrimaryRelations); + const primaryDomainsEntities = await getEntities(path.resolve(__dirname, PRIMARY_DOMAIN_DATA_PATH)); + const savedPrimaryDomains = await loadAndSaveData(Domain, dataSource, primaryDomainsEntities, domainPrimaryRelations); const domainRedirectedRelations = { project: savedProjects, redirectTo: savedPrimaryDomains }; - await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH), domainRedirectedRelations); + const redirectDomainsEntities = await getEntities(path.resolve(__dirname, REDIRECTED_DOMAIN_DATA_PATH)); + await loadAndSaveData(Domain, dataSource, redirectDomainsEntities, domainRedirectedRelations); const savedDomains = await domainRepository.find(); @@ -87,14 +65,16 @@ const generateTestData = async (dataSource: DataSource) => { organization: savedOrgs }; - await loadAndSaveData(UserOrganization, dataSource, path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH), userOrganizationRelations); + const userOrganizationsEntities = await getEntities(path.resolve(__dirname, USER_ORGANIZATION_DATA_PATH)); + await loadAndSaveData(UserOrganization, dataSource, userOrganizationsEntities, userOrganizationRelations); const projectMemberRelations = { member: savedUsers, project: savedProjects }; - await loadAndSaveData(ProjectMember, dataSource, path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH), projectMemberRelations); + const projectMembersEntities = await getEntities(path.resolve(__dirname, PROJECT_MEMBER_DATA_PATH)); + await loadAndSaveData(ProjectMember, dataSource, projectMembersEntities, projectMemberRelations); const deploymentRelations = { project: savedProjects, @@ -102,23 +82,15 @@ const generateTestData = async (dataSource: DataSource) => { createdBy: savedUsers }; - await loadAndSaveData(Deployment, dataSource, path.resolve(__dirname, DEPLOYMENT_DATA_PATH), deploymentRelations); + const deploymentsEntities = await getEntities(path.resolve(__dirname, DEPLOYMENT_DATA_PATH)); + await loadAndSaveData(Deployment, dataSource, deploymentsEntities, deploymentRelations); const environmentVariableRelations = { project: savedProjects }; - await loadAndSaveData(EnvironmentVariable, dataSource, path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH), environmentVariableRelations); -}; - -const checkFileExists = async (filePath: string) => { - try { - await fs.access(filePath, fs.constants.F_OK); - return true; - } catch (err) { - log(err); - return false; - } + const environmentVariablesEntities = await getEntities(path.resolve(__dirname, ENVIRONMENT_VARIABLE_DATA_PATH)); + await loadAndSaveData(EnvironmentVariable, dataSource, environmentVariablesEntities, environmentVariableRelations); }; const main = async () => { diff --git a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx index 958382ac..7049a270 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx @@ -16,6 +16,7 @@ const DEFAULT_FILTER_VALUE: FilterValue = { searchedBranch: '', status: StatusOptions.ALL_STATUS, }; +const FETCH_DEPLOYMENTS_INTERVAL = 5000; const DeploymentsTabPanel = () => { const client = useGQLClient(); @@ -26,22 +27,30 @@ const DeploymentsTabPanel = () => { const [deployments, setDeployments] = useState([]); const [prodBranchDomains, setProdBranchDomains] = useState([]); - const fetchDeployments = async () => { + const fetchDeployments = useCallback(async () => { const { deployments } = await client.getDeployments(project.id); setDeployments(deployments); - }; + }, [client]); const fetchProductionBranchDomains = useCallback(async () => { const { domains } = await client.getDomains(project.id, { branch: project.prodBranch, }); setProdBranchDomains(domains); - }, []); + }, [client]); useEffect(() => { - fetchDeployments(); fetchProductionBranchDomains(); - }, []); + fetchDeployments(); + + const interval = setInterval(() => { + fetchDeployments(); + }, FETCH_DEPLOYMENTS_INTERVAL); + + return () => { + clearInterval(interval); + }; + }, [fetchDeployments, fetchProductionBranchDomains]); const currentDeployment = useMemo(() => { return deployments.find((deployment) => { From 4d0f2ca893e7837ea48860ffc9ed23d6009815cf Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Wed, 21 Feb 2024 13:53:05 -0600 Subject: [PATCH 07/12] SO webapp-ify the frontend (#82) * webapp-ify * Tweak values * Self-contained --- build-webapp.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100755 build-webapp.sh diff --git a/build-webapp.sh b/build-webapp.sh new file mode 100755 index 00000000..e3e8db99 --- /dev/null +++ b/build-webapp.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +PKG_DIR="./packages/frontend" +OUTPUT_DIR="${PKG_DIR}/build" +DEST_DIR=${1:-/data} + +if [[ -d "$DEST_DIR" ]]; then + echo "${DEST_DIR} already exists." 1>&2 + exit 1 +fi + +cat > $PKG_DIR/.env <&2 + exit 1 +fi + +mv "$OUTPUT_DIR" "$DEST_DIR" From e816c596ca81ae7b5cab3bb65adda651bd81cb06 Mon Sep 17 00:00:00 2001 From: Nabarun Gogoi Date: Thu, 22 Feb 2024 10:04:33 +0530 Subject: [PATCH 08/12] Publish `ApplicationRecord` and `ApplicationDeploymentRequest` on creating new deployments (#89) * Create application deployment request on new deployment * Save application deployment request datas in deployment entity * Add project as dependency * Use project repo name as record app name --------- Co-authored-by: neeraj --- packages/backend/src/entity/Deployment.ts | 15 ++++++ packages/backend/src/entity/Project.ts | 15 ------ packages/backend/src/registry.ts | 25 +++++----- packages/backend/src/service.ts | 49 ++++++++++--------- packages/backend/src/types.ts | 4 +- .../backend/test/fixtures/deployments.json | 22 +++++++++ packages/backend/test/fixtures/projects.json | 10 ---- .../backend/test/publish-deploy-records.ts | 2 +- .../org-slug/projects/id/Deployments.tsx | 4 +- 9 files changed, 78 insertions(+), 68 deletions(-) diff --git a/packages/backend/src/entity/Deployment.ts b/packages/backend/src/entity/Deployment.ts index 62d3f38f..5771b5d2 100644 --- a/packages/backend/src/entity/Deployment.ts +++ b/packages/backend/src/entity/Deployment.ts @@ -26,6 +26,15 @@ export enum DeploymentStatus { Error = 'Error', } +export interface ApplicationDeploymentRequest { + type: string + version: string + name: string + application: string + config: string, + meta: string +} + export interface ApplicationRecord { type: string; version:string @@ -78,6 +87,12 @@ export class Deployment { @Column('simple-json') applicationRecordData!: ApplicationRecord; + @Column('varchar') + applicationDeploymentRequestId!: string; + + @Column('simple-json') + applicationDeploymentRequestData!: ApplicationDeploymentRequest; + @Column('varchar', { nullable: true }) applicationDeploymentRecordId!: string | null; diff --git a/packages/backend/src/entity/Project.ts b/packages/backend/src/entity/Project.ts index 86fa7b3d..4415476d 100644 --- a/packages/backend/src/entity/Project.ts +++ b/packages/backend/src/entity/Project.ts @@ -15,15 +15,6 @@ import { Organization } from './Organization'; import { ProjectMember } from './ProjectMember'; import { Deployment } from './Deployment'; -export interface ApplicationDeploymentRequest { - type: string - version: string - name: string - application: string - config: string, - meta: string -} - @Entity() export class Project { @PrimaryGeneratedColumn('uuid') @@ -52,12 +43,6 @@ export class Project { @Column('varchar', { length: 255, default: 'main' }) prodBranch!: string; - @Column('varchar', { nullable: true }) - applicationDeploymentRequestId!: string | null; - - @Column('simple-json', { nullable: true }) - applicationDeploymentRequestData!: ApplicationDeploymentRequest | null; - @Column('text', { default: '' }) description!: string; diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index 108318bb..e3bce569 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -6,8 +6,7 @@ import { DateTime } from 'luxon'; import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk'; import { RegistryConfig } from './config'; -import { ApplicationDeploymentRequest } from './entity/Project'; -import { ApplicationRecord, Deployment } from './entity/Deployment'; +import { ApplicationRecord, Deployment, ApplicationDeploymentRequest } from './entity/Deployment'; import { AppDeploymentRecord, PackageJSON } from './types'; const log = debug('snowball:registry'); @@ -27,17 +26,18 @@ export class Registry { } async createApplicationRecord ({ + appName, packageJSON, commitHash, appType, repoUrl }: { + appName: string, packageJSON: PackageJSON commitHash: string, appType: string, repoUrl: string }): Promise<{applicationRecordId: string, applicationRecordData: ApplicationRecord}> { - assert(packageJSON.name, "name field doesn't exist in package.json"); // 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 @@ -60,7 +60,7 @@ export class Registry { repository_ref: commitHash, repository: [repoUrl], app_type: appType, - name: packageJSON.name, + name: appName, ...(packageJSON.description && { description: packageJSON.description }), ...(packageJSON.homepage && { homepage: packageJSON.homepage }), ...(packageJSON.license && { license: packageJSON.license }), @@ -81,7 +81,7 @@ export class Registry { log('Application record data:', applicationRecord); // TODO: Discuss computation of CRN - const crn = this.getCrn(packageJSON.name); + const crn = this.getCrn(packageJSON.name, appName); log(`Setting name: ${crn} for record ID: ${result.data.id}`); await this.registry.setName({ cid: result.data.id, crn }, this.registryConfig.privateKey, this.registryConfig.fee); @@ -93,6 +93,7 @@ export class Registry { async createApplicationDeploymentRequest (data: { appName: string, + packageJsonName: string, commitHash: string, repository: string, environmentVariables: { [key: string]: string } @@ -100,7 +101,7 @@ export class Registry { applicationDeploymentRequestId: string, applicationDeploymentRequestData: ApplicationDeploymentRequest }> { - const crn = this.getCrn(data.appName); + const crn = this.getCrn(data.packageJsonName, data.appName); const records = await this.registry.resolveNames([crn]); const applicationRecord = records[0]; @@ -159,14 +160,10 @@ export class Registry { return records.filter((record: AppDeploymentRecord) => deployments.some(deployment => deployment.applicationRecordId === record.attributes.application)); } - getCrn (packageJsonName: string): string { - const [arg1, arg2] = packageJsonName.split('/'); + getCrn (packageJsonName: string, appName: string): string { + const [arg1] = packageJsonName.split('/'); + const authority = arg1.replace('@', ''); - if (arg2) { - const authority = arg1.replace('@', ''); - return `crn://${authority}/applications/${arg2}`; - } - - return `crn://${arg1}/applications/${arg1}`; + return `crn://${authority}/applications/${appName}`; } } diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index 0ce326ff..401e9a18 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -15,7 +15,7 @@ import { Permission, ProjectMember } from './entity/ProjectMember'; import { User } from './entity/User'; import { Registry } from './registry'; import { GitHubConfig, RegistryConfig } from './config'; -import { AppDeploymentRecord, GitPushEventPayload } from './types'; +import { AppDeploymentRecord, GitPushEventPayload, PackageJSON } from './types'; const log = debug('snowball:service'); @@ -326,7 +326,9 @@ export class Service { } assert(!Array.isArray(packageJSONData) && packageJSONData.type === 'file'); - const packageJSON = JSON.parse(atob(packageJSONData.content)); + const packageJSON: PackageJSON = JSON.parse(atob(packageJSONData.content)); + + assert(packageJSON.name, "name field doesn't exist in package.json"); if (!recordData.repoUrl) { const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo }); @@ -335,12 +337,30 @@ export class Service { // TODO: Set environment variables for each deployment (environment variables can`t be set in application record) const { applicationRecordId, applicationRecordData } = await this.registry.createApplicationRecord({ + appName: repo, packageJSON, appType: data.project!.template!, commitHash: data.commitHash!, repoUrl: recordData.repoUrl }); + const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(data.project.id!, { environment: Environment.Production }); + + const environmentVariablesObj = environmentVariables.reduce((acc, env) => { + acc[env.key] = env.value; + + return acc; + }, {} as { [key: string]: string }); + + const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest( + { + appName: repo, + packageJsonName: packageJSON.name, + commitHash: data.commitHash!, + repository: recordData.repoUrl, + environmentVariables: environmentVariablesObj + }); + // Update previous deployment with prod branch domain // TODO: Fix unique constraint error for domain await this.db.updateDeployment({ @@ -358,6 +378,8 @@ export class Service { status: DeploymentStatus.Building, applicationRecordId, applicationRecordData, + applicationDeploymentRequestId, + applicationDeploymentRequestData, domain: data.domain, createdBy: Object.assign(new User(), { id: userId @@ -393,7 +415,7 @@ export class Service { const { data: repoDetails } = await octokit.rest.repos.get({ owner, repo }); // Create deployment with prod branch and latest commit - const newDeployment = await this.createDeployment(userId, + await this.createDeployment(userId, octokit, { project, @@ -408,27 +430,6 @@ export class Service { } ); - const environmentVariables = await this.db.getEnvironmentVariablesByProjectId(project.id, { environment: Environment.Production }); - - const environmentVariablesObj = environmentVariables.reduce((acc, env) => { - acc[env.key] = env.value; - - return acc; - }, {} as { [key: string]: string }); - - const { applicationDeploymentRequestId, applicationDeploymentRequestData } = await this.registry.createApplicationDeploymentRequest( - { - appName: newDeployment.applicationRecordData.name!, - commitHash: latestCommit.sha, - repository: repoDetails.html_url, - environmentVariables: environmentVariablesObj - }); - - await this.db.updateProjectById(project.id, { - applicationDeploymentRequestId, - applicationDeploymentRequestData - }); - await this.createRepoHook(octokit, project); return project; diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 71626645..e0cef120 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -1,6 +1,6 @@ export interface PackageJSON { - name?: string; - version?: string; + name: string; + version: string; author?: string; description?: string; homepage?: string; diff --git a/packages/backend/test/fixtures/deployments.json b/packages/backend/test/fixtures/deployments.json index 5eb682a8..c0536072 100644 --- a/packages/backend/test/fixtures/deployments.json +++ b/packages/backend/test/fixtures/deployments.json @@ -9,6 +9,8 @@ "isCurrent": true, "applicationRecordId": "qbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "xqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "main", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -24,6 +26,8 @@ "isCurrent": false, "applicationRecordId": "wbafyreihvzya6ovp4yfpkqnddkui2iw7thbhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "wqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -39,6 +43,8 @@ "isCurrent": false, "applicationRecordId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "kqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -54,6 +60,8 @@ "isCurrent": false, "applicationRecordId": "rbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhw74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "yqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "prod", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -69,6 +77,8 @@ "isCurrent": true, "applicationRecordId": "tbafyreihvzya6ovp4yfpqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "main", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -84,6 +94,8 @@ "isCurrent": false, "applicationRecordId": "ybafyreihvzya6ovp4yfpkqnddkui2iw7t6bhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "tqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -99,6 +111,8 @@ "isCurrent": false, "applicationRecordId": "ubafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "eqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -114,6 +128,8 @@ "isCurrent": true, "applicationRecordId": "ibayreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "dqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "main", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -129,6 +145,8 @@ "isCurrent": false, "applicationRecordId": "obafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", "applicationRecordData": {}, + "applicationDeploymentRequestId": "aqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -144,6 +162,8 @@ "isCurrent": false, "applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", "applicationRecordData": {}, + "applicationDeploymentRequestId": "uqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", @@ -159,6 +179,8 @@ "isCurrent": true, "applicationRecordId": "pbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowo", "applicationRecordData": {}, + "applicationDeploymentRequestId": "pqbafyrehvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", + "applicationDeploymentRequestData": {}, "branch": "test", "commitHash": "d5dfd7b827226b0d09d897346d291c256e113e00", "commitMessage": "subscription added", diff --git a/packages/backend/test/fixtures/projects.json b/packages/backend/test/fixtures/projects.json index 55c5ee23..f4564643 100644 --- a/packages/backend/test/fixtures/projects.json +++ b/packages/backend/test/fixtures/projects.json @@ -10,8 +10,6 @@ "framework": "test", "webhooks": [], "icon": "", - "applicationDeploymentRequestId": "hbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, "subDomain": "testProject.snowball.xyz" }, { @@ -25,8 +23,6 @@ "framework": "test-2", "webhooks": [], "icon": "", - "applicationDeploymentRequestId": "gbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, "subDomain": "testProject-2.snowball.xyz" }, { @@ -40,8 +36,6 @@ "framework": "test-3", "webhooks": [], "icon": "", - "applicationDeploymentRequestId": "ebafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, "subDomain": "iglootools.snowball.xyz" }, { @@ -55,8 +49,6 @@ "framework": "test-4", "webhooks": [], "icon": "", - "applicationDeploymentRequestId": "qbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, "subDomain": "iglootools-2.snowball.xyz" }, { @@ -70,8 +62,6 @@ "framework": "test-5", "webhooks": [], "icon": "", - "applicationDeploymentRequestId": "xbafyreihvzya6ovp4yfpkqnddkui2iw7t6hbhwq74lbqs7bhobvmfhrowoi", - "applicationDeploymentRequestData": {}, "subDomain": "snowball-2.snowball.xyz" } ] diff --git a/packages/backend/test/publish-deploy-records.ts b/packages/backend/test/publish-deploy-records.ts index f5636c4e..90ccc16e 100644 --- a/packages/backend/test/publish-deploy-records.ts +++ b/packages/backend/test/publish-deploy-records.ts @@ -53,7 +53,7 @@ async function main () { so: '66fcfa49a1664d4cb4ce4f72c1c0e151' }), - request: deployment.project.applicationDeploymentRequestId, + request: deployment.applicationDeploymentRequestId, url }; diff --git a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx index 7049a270..1d9e4e2d 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx @@ -30,14 +30,14 @@ const DeploymentsTabPanel = () => { const fetchDeployments = useCallback(async () => { const { deployments } = await client.getDeployments(project.id); setDeployments(deployments); - }, [client]); + }, [client, project]); const fetchProductionBranchDomains = useCallback(async () => { const { domains } = await client.getDomains(project.id, { branch: project.prodBranch, }); setProdBranchDomains(domains); - }, [client]); + }, [client, project]); useEffect(() => { fetchProductionBranchDomains(); From 9dec48d67c6a0920e2713a041cd9b673748defa9 Mon Sep 17 00:00:00 2001 From: Wahyu Kurniawan Date: Thu, 22 Feb 2024 12:24:25 +0700 Subject: [PATCH 09/12] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20add=20di?= =?UTF-8?q?sabled=20button=20example=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/src/pages/components/index.tsx | 32 ++++--- .../src/pages/components/renders/button.tsx | 90 ++++++++++++------- 2 files changed, 78 insertions(+), 44 deletions(-) diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index b2c81d31..67e68e0b 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -10,6 +10,7 @@ import { renderBadges } from './renders/badge'; import { renderButtonIcons, renderButtons, + renderDisabledButtons, renderLinks, } from './renders/button'; import { @@ -53,6 +54,27 @@ const Page = () => { {renderButtonIcons()}
+ {/* Link */} +
+

Link

+
+ {renderLinks()} +
+
+ + {/* Disabled button, icon only, and link */} +
+
+

Disabled

+

+ Button – icon only – link +

+
+
+ {renderDisabledButtons()} +
+
+
{/* Badge */} @@ -139,16 +161,6 @@ const Page = () => { {renderInlineNotificationWithDescriptions()}
- -
- - {/* Link */} -
-

Link

-
- {renderLinks()} -
-
diff --git a/packages/frontend/src/pages/components/renders/button.tsx b/packages/frontend/src/pages/components/renders/button.tsx index f5272d3e..0b86aaaf 100644 --- a/packages/frontend/src/pages/components/renders/button.tsx +++ b/packages/frontend/src/pages/components/renders/button.tsx @@ -2,44 +2,45 @@ import { Button, ButtonTheme } from 'components/shared/Button'; import { PlusIcon } from 'components/shared/CustomIcon'; import React from 'react'; +const buttonVariants = ['primary', 'secondary', 'tertiary', 'danger'] as const; +const buttonSizes = ['lg', 'md', 'sm', 'xs'] as const; +const iconOnlyVariants = [ + 'primary', + 'secondary', + 'tertiary', + 'ghost', + 'danger', + 'danger-ghost', +] as const; +const linkVariants = ['link', 'link-emphasized'] as const; + export const renderButtons = () => { - return ['primary', 'secondary', 'tertiary', 'danger'].map( - (variant, index) => ( -
- {['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => ( - - ))} -
- ), - ); + return buttonVariants.map((variant, index) => ( +
+ {buttonSizes.map((size) => ( + + ))} +
+ )); }; export const renderButtonIcons = () => { - return [ - 'primary', - 'secondary', - 'tertiary', - 'ghost', - 'danger', - 'danger-ghost', - ].map((variant, index) => ( + return iconOnlyVariants.map((variant, index) => (
- {['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => ( + {buttonSizes.map((size) => ( @@ -49,17 +50,38 @@ export const renderButtonIcons = () => { }; export const renderLinks = () => { - return ['link', 'link-emphasized', 'disabled'].map((variant) => ( + return linkVariants.map((variant) => ( )); }; + +export const renderDisabledButtons = () => { + return ( + <> + {/* Button variants */} + + {/* Icon only variants */} + + {/* Link variantws */} + + + ); +}; From 30bbe4d766de1741000a1573fc28b0355a9eb36e Mon Sep 17 00:00:00 2001 From: Zachery Date: Thu, 22 Feb 2024 18:25:04 +0800 Subject: [PATCH 10/12] [T-4870] Tooltip component (#96) * fix: button forwardRef * feat: tooltip component --- packages/frontend/package.json | 1 + .../src/components/shared/Button/Button.tsx | 20 ++- .../shared/Tooltip/Tooltip.theme.ts | 14 ++ .../src/components/shared/Tooltip/Tooltip.tsx | 47 +++++++ .../components/shared/Tooltip/TooltipBase.tsx | 38 ++++++ .../Tooltip/TooltipContent/TooltipContent.tsx | 46 +++++++ .../shared/Tooltip/TooltipContent/index.ts | 1 + .../Tooltip/TooltipTrigger/TooltipTrigger.tsx | 12 ++ .../shared/Tooltip/TooltipTrigger/index.ts | 1 + .../src/components/shared/Tooltip/index.ts | 2 + .../frontend/src/pages/components/index.tsx | 11 +- .../src/pages/components/renders/tooltip.tsx | 30 +++++ packages/frontend/tailwind.config.js | 9 +- yarn.lock | 122 ++++++++++++++++++ 14 files changed, 344 insertions(+), 10 deletions(-) create mode 100644 packages/frontend/src/components/shared/Tooltip/Tooltip.theme.ts create mode 100644 packages/frontend/src/components/shared/Tooltip/Tooltip.tsx create mode 100644 packages/frontend/src/components/shared/Tooltip/TooltipBase.tsx create mode 100644 packages/frontend/src/components/shared/Tooltip/TooltipContent/TooltipContent.tsx create mode 100644 packages/frontend/src/components/shared/Tooltip/TooltipContent/index.ts create mode 100644 packages/frontend/src/components/shared/Tooltip/TooltipTrigger/TooltipTrigger.tsx create mode 100644 packages/frontend/src/components/shared/Tooltip/TooltipTrigger/index.ts create mode 100644 packages/frontend/src/components/shared/Tooltip/index.ts create mode 100644 packages/frontend/src/pages/components/renders/tooltip.tsx diff --git a/packages/frontend/package.json b/packages/frontend/package.json index f31fa586..8daa876c 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -8,6 +8,7 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-tooltip": "^1.0.7", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/packages/frontend/src/components/shared/Button/Button.tsx b/packages/frontend/src/components/shared/Button/Button.tsx index af845e0c..968cca72 100644 --- a/packages/frontend/src/components/shared/Button/Button.tsx +++ b/packages/frontend/src/components/shared/Button/Button.tsx @@ -98,19 +98,29 @@ const Button = forwardRef< href, ...baseLinkProps, }; - return {_children}; + return ( + // @ts-expect-error - ref + + {_children} + + ); } // Internal link return ( - + // @ts-expect-error - ref + {_children} ); } else { const { ...buttonProps } = _props; - // @ts-expect-error - as prop is not a valid prop for button elements - return ; + return ( + // @ts-expect-error - as prop is not a valid prop for button elements + + ); } }, [], @@ -161,8 +171,6 @@ const Button = forwardRef< return ( {cloneIcon(leftIcon, { ...iconSize })} diff --git a/packages/frontend/src/components/shared/Tooltip/Tooltip.theme.ts b/packages/frontend/src/components/shared/Tooltip/Tooltip.theme.ts new file mode 100644 index 00000000..c4c1c5e0 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/Tooltip.theme.ts @@ -0,0 +1,14 @@ +import { tv } from 'tailwind-variants'; + +export const tooltipTheme = tv({ + slots: { + content: [ + 'z-tooltip', + 'rounded-md', + 'bg-surface-high-contrast', + 'p-2', + 'text-elements-on-high-contrast', + ], + arrow: ['fill-surface-high-contrast'], + }, +}); diff --git a/packages/frontend/src/components/shared/Tooltip/Tooltip.tsx b/packages/frontend/src/components/shared/Tooltip/Tooltip.tsx new file mode 100644 index 00000000..75d73c78 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/Tooltip.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import type { + TooltipContentProps, + TooltipTriggerProps, +} from '@radix-ui/react-tooltip'; +import { ReactNode, useState } from 'react'; + +import { TooltipBase, type TooltipBaseProps } from './TooltipBase'; + +export interface TooltipProps extends TooltipBaseProps { + triggerProps?: TooltipTriggerProps; + contentProps?: TooltipContentProps; + content?: ReactNode; +} + +// https://github.com/radix-ui/primitives/issues/955#issuecomment-1798201143 +// Wrap on top of Tooltip base to make tooltip open on mobile via click +export const Tooltip = ({ + children, + triggerProps, + contentProps, + content, + ...props +}: TooltipProps) => { + const [isTooltipVisible, setIsTooltipVisible] = useState(false); + + return ( + + setIsTooltipVisible(false)} + onClick={() => setIsTooltipVisible((prevOpen) => !prevOpen)} + onFocus={() => setTimeout(() => setIsTooltipVisible(true), 0)} + {...triggerProps} + > + {triggerProps?.children ?? children} + + + {content ?? contentProps?.children ?? 'Coming soon'} + + + ); +}; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipBase.tsx b/packages/frontend/src/components/shared/Tooltip/TooltipBase.tsx new file mode 100644 index 00000000..273a9cbe --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipBase.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { + Provider, + TooltipProps as RadixTooltipProps, + Root, + type TooltipProviderProps, +} from '@radix-ui/react-tooltip'; +import { useState, type PropsWithChildren } from 'react'; + +import { TooltipContent } from './TooltipContent'; +import { TooltipTrigger } from './TooltipTrigger'; + +export interface TooltipBaseProps extends RadixTooltipProps { + providerProps?: TooltipProviderProps; +} + +export const TooltipBase = ({ + children, + providerProps, + ...props +}: PropsWithChildren) => { + const [isTooltipVisible, setIsTooltipVisible] = useState(false); + + return ( + + + {children} + + + ); +}; + +TooltipBase.Trigger = TooltipTrigger; +TooltipBase.Content = TooltipContent; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipContent/TooltipContent.tsx b/packages/frontend/src/components/shared/Tooltip/TooltipContent/TooltipContent.tsx new file mode 100644 index 00000000..57678206 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipContent/TooltipContent.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { + Arrow, + Content, + Portal, + type TooltipArrowProps, + type TooltipContentProps, +} from '@radix-ui/react-tooltip'; + +import { tooltipTheme } from '../Tooltip.theme'; + +export interface ContentProps extends TooltipContentProps { + hasArrow?: boolean; + arrowProps?: TooltipArrowProps; +} + +export const TooltipContent = ({ + children, + arrowProps, + className, + hasArrow = true, + ...props +}: ContentProps) => { + const { content, arrow } = tooltipTheme(); + return ( + + + {hasArrow && ( + + )} + {children} + + + ); +}; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipContent/index.ts b/packages/frontend/src/components/shared/Tooltip/TooltipContent/index.ts new file mode 100644 index 00000000..1675248c --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipContent/index.ts @@ -0,0 +1 @@ +export * from './TooltipContent'; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/TooltipTrigger.tsx b/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/TooltipTrigger.tsx new file mode 100644 index 00000000..c27993ed --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/TooltipTrigger.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Trigger, type TooltipTriggerProps } from '@radix-ui/react-tooltip'; + +export type TriggerProps = TooltipTriggerProps; + +export const TooltipTrigger = ({ children, ...props }: TriggerProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/index.ts b/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/index.ts new file mode 100644 index 00000000..e73d66c6 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/index.ts @@ -0,0 +1 @@ +export * from './TooltipTrigger'; diff --git a/packages/frontend/src/components/shared/Tooltip/index.ts b/packages/frontend/src/components/shared/Tooltip/index.ts new file mode 100644 index 00000000..a51f2a31 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/index.ts @@ -0,0 +1,2 @@ +export * from './Tooltip'; +export * from './TooltipBase'; diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index 67e68e0b..bfe02385 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -23,6 +23,7 @@ import { renderInlineNotifications, } from './renders/inlineNotifications'; import { renderInputs } from './renders/input'; +import { renderTooltips } from './renders/tooltip'; const Page = () => { const [singleDate, setSingleDate] = useState(); @@ -41,13 +42,21 @@ const Page = () => {
- {/* Button */}
+

Tooltip

+
+ {renderTooltips()} +
+ +
+ + {/* Input */}

Input

{renderInputs()}
+ {/* Button */}

Button

{renderButtons()} diff --git a/packages/frontend/src/pages/components/renders/tooltip.tsx b/packages/frontend/src/pages/components/renders/tooltip.tsx new file mode 100644 index 00000000..7114f4db --- /dev/null +++ b/packages/frontend/src/pages/components/renders/tooltip.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Button } from 'components/shared/Button'; +import { Tooltip } from 'components/shared/Tooltip'; +import { ContentProps } from 'components/shared/Tooltip/TooltipContent'; + +const alignments: ContentProps['align'][] = ['start', 'center', 'end']; +const sides: ContentProps['side'][] = ['left', 'top', 'bottom', 'right']; + +export const renderTooltips = () => { + const tooltips = sides.map((side) => { + return alignments.map((align) => { + return ( + + + + ); + }); + }); + return tooltips; +}; diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js index 4bcb0b01..9a814026 100644 --- a/packages/frontend/tailwind.config.js +++ b/packages/frontend/tailwind.config.js @@ -8,10 +8,13 @@ export default withMT({ '../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}', ], theme: { - fontFamily: { - sans: ['Inter', 'sans-serif'], - }, extend: { + zIndex: { + tooltip: '52', + }, + fontFamily: { + sans: ['Inter', 'sans-serif'], + }, fontSize: { '2xs': '0.625rem', '3xs': '0.5rem', diff --git a/yarn.lock b/yarn.lock index c8881430..f4d59093 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2032,6 +2032,13 @@ link-module-alias "^1.2.0" shx "^0.3.4" +"@floating-ui/core@^1.0.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1" + integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g== + dependencies: + "@floating-ui/utils" "^0.2.1" + "@floating-ui/core@^1.4.2": version "1.5.2" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.2.tgz#53a0f7a98c550e63134d504f26804f6b83dbc071" @@ -2047,6 +2054,14 @@ "@floating-ui/core" "^1.4.2" "@floating-ui/utils" "^0.1.3" +"@floating-ui/dom@^1.6.1": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef" + integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw== + dependencies: + "@floating-ui/core" "^1.0.0" + "@floating-ui/utils" "^0.2.0" + "@floating-ui/react-dom@^1.2.2": version "1.3.0" resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.3.0.tgz#4d35d416eb19811c2b0e9271100a6aa18c1579b3" @@ -2054,6 +2069,13 @@ dependencies: "@floating-ui/dom" "^1.2.1" +"@floating-ui/react-dom@^2.0.0": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d" + integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw== + dependencies: + "@floating-ui/dom" "^1.6.1" + "@floating-ui/react@0.19.0": version "0.19.0" resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.19.0.tgz#d8e19a3fcfaa0684d5ec3f335232b4e0ac0c87e1" @@ -2068,6 +2090,11 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9" integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A== +"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" + integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== + "@fontsource/inter@^5.0.16": version "5.0.16" resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.0.16.tgz#b858508cdb56dcbbf3166903122851e2fbd16b50" @@ -3284,6 +3311,14 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-arrow@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d" + integrity sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-avatar@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623" @@ -3342,6 +3377,18 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-dismissable-layer@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4" + integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-escape-keydown" "1.0.3" + "@radix-ui/react-id@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0" @@ -3350,6 +3397,31 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "1.0.1" +"@radix-ui/react-popper@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42" + integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w== + dependencies: + "@babel/runtime" "^7.13.10" + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-use-rect" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/rect" "1.0.1" + +"@radix-ui/react-portal@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15" + integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-presence@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" @@ -3406,6 +3478,25 @@ "@radix-ui/react-roving-focus" "1.0.4" "@radix-ui/react-use-controllable-state" "1.0.1" +"@radix-ui/react-tooltip@^1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz#8f55070f852e7e7450cc1d9210b793d2e5a7686e" + integrity sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-popper" "1.1.3" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-visually-hidden" "1.0.3" + "@radix-ui/react-use-callback-ref@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a" @@ -3421,6 +3512,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-use-escape-keydown@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755" + integrity sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399" @@ -3435,6 +3534,14 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-use-rect@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2" + integrity sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/rect" "1.0.1" + "@radix-ui/react-use-size@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2" @@ -3443,6 +3550,21 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "1.0.1" +"@radix-ui/react-visually-hidden@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz#51aed9dd0fe5abcad7dee2a234ad36106a6984ac" + integrity sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + +"@radix-ui/rect@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f" + integrity sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@remix-run/router@1.13.1": version "1.13.1" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.13.1.tgz#07e2a8006f23a3bc898b3f317e0a58cc8076b86e" From d2ca4df35a27f2cb424bef31923808a3eae277e8 Mon Sep 17 00:00:00 2001 From: Wahyu Kurniawan Date: Thu, 22 Feb 2024 17:30:33 +0700 Subject: [PATCH 11/12] [T-4864: feat] Radio component (#90) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🎨 style: make the cursor of the tab trigger wrapper to default * ⚡️ feat: create radio component * 📝 docs: add radio component to the example page * 🔧 chore: install `@radix-ui/react-radio-group` --- packages/frontend/package.json | 1 + .../components/shared/Radio/Radio.theme.ts | 54 ++++++++++++++ .../src/components/shared/Radio/Radio.tsx | 63 ++++++++++++++++ .../src/components/shared/Radio/RadioItem.tsx | 74 +++++++++++++++++++ .../src/components/shared/Radio/index.ts | 2 + .../src/components/shared/Tabs/Tabs.theme.ts | 2 + .../frontend/src/pages/components/index.tsx | 24 ++++++ .../src/pages/components/renders/radio.ts | 16 ++++ yarn.lock | 17 +++++ 9 files changed, 253 insertions(+) create mode 100644 packages/frontend/src/components/shared/Radio/Radio.theme.ts create mode 100644 packages/frontend/src/components/shared/Radio/Radio.tsx create mode 100644 packages/frontend/src/components/shared/Radio/RadioItem.tsx create mode 100644 packages/frontend/src/components/shared/Radio/index.ts create mode 100644 packages/frontend/src/pages/components/renders/radio.ts diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 8daa876c..2e040901 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -7,6 +7,7 @@ "@material-tailwind/react": "^2.1.7", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", "@testing-library/jest-dom": "^5.17.0", diff --git a/packages/frontend/src/components/shared/Radio/Radio.theme.ts b/packages/frontend/src/components/shared/Radio/Radio.theme.ts new file mode 100644 index 00000000..0b84601e --- /dev/null +++ b/packages/frontend/src/components/shared/Radio/Radio.theme.ts @@ -0,0 +1,54 @@ +import { VariantProps, tv } from 'tailwind-variants'; + +export const radioTheme = tv({ + slots: { + root: ['flex', 'gap-3', 'flex-wrap'], + wrapper: ['flex', 'items-center', 'gap-2', 'group'], + label: ['text-sm', 'tracking-[-0.006em]', 'text-elements-high-em'], + radio: [ + 'w-5', + 'h-5', + 'rounded-full', + 'border', + 'group', + 'border-border-interactive/10', + 'shadow-button', + 'group-hover:border-border-interactive/[0.14]', + 'focus-ring', + // Checked + 'data-[state=checked]:bg-controls-primary', + 'data-[state=checked]:group-hover:bg-controls-primary-hovered', + ], + indicator: [ + 'flex', + 'items-center', + 'justify-center', + 'relative', + 'w-full', + 'h-full', + 'after:content-[""]', + 'after:block', + 'after:w-2.5', + 'after:h-2.5', + 'after:rounded-full', + 'after:bg-transparent', + 'after:group-hover:bg-controls-disabled', + 'after:group-focus-visible:bg-controls-disabled', + // Checked + 'after:data-[state=checked]:bg-elements-on-primary', + 'after:data-[state=checked]:group-hover:bg-elements-on-primary', + 'after:data-[state=checked]:group-focus-visible:bg-elements-on-primary', + ], + }, + variants: { + orientation: { + vertical: { root: ['flex-col'] }, + horizontal: { root: ['flex-row'] }, + }, + }, + defaultVariants: { + orientation: 'vertical', + }, +}); + +export type RadioTheme = VariantProps; diff --git a/packages/frontend/src/components/shared/Radio/Radio.tsx b/packages/frontend/src/components/shared/Radio/Radio.tsx new file mode 100644 index 00000000..96542493 --- /dev/null +++ b/packages/frontend/src/components/shared/Radio/Radio.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { + Root as RadixRoot, + RadioGroupProps, +} from '@radix-ui/react-radio-group'; +import { RadioTheme, radioTheme } from './Radio.theme'; +import { RadioItem, RadioItemProps } from './RadioItem'; + +export interface RadioOption extends RadioItemProps { + /** + * The label of the radio option. + */ + label: string; + /** + * The value of the radio option. + */ + value: string; +} + +export interface RadioProps extends RadioGroupProps, RadioTheme { + /** + * The options of the radio. + * @default [] + * @example + * ```tsx + * const options = [ + * { + * label: 'Label 1', + * value: '1', + * }, + * { + * label: 'Label 2', + * value: '2', + * }, + * { + * label: 'Label 3', + * value: '3', + * }, + * ]; + * ``` + */ + options: RadioOption[]; +} + +/** + * The Radio component is used to select one option from a list of options. + */ +export const Radio = ({ + className, + options, + orientation, + ...props +}: RadioProps) => { + const { root } = radioTheme({ orientation }); + + return ( + + {options.map((option) => ( + + ))} + + ); +}; diff --git a/packages/frontend/src/components/shared/Radio/RadioItem.tsx b/packages/frontend/src/components/shared/Radio/RadioItem.tsx new file mode 100644 index 00000000..177af9db --- /dev/null +++ b/packages/frontend/src/components/shared/Radio/RadioItem.tsx @@ -0,0 +1,74 @@ +import React, { ComponentPropsWithoutRef } from 'react'; +import { + Item as RadixRadio, + Indicator as RadixIndicator, + RadioGroupItemProps, + RadioGroupIndicatorProps, +} from '@radix-ui/react-radio-group'; +import { radioTheme } from './Radio.theme'; + +export interface RadioItemProps extends RadioGroupItemProps { + /** + * The wrapper props of the radio item. + * You can use this prop to customize the wrapper props. + */ + wrapperProps?: ComponentPropsWithoutRef<'div'>; + /** + * The label props of the radio item. + * You can use this prop to customize the label props. + */ + labelProps?: ComponentPropsWithoutRef<'label'>; + /** + * The indicator props of the radio item. + * You can use this prop to customize the indicator props. + */ + indicatorProps?: RadioGroupIndicatorProps; + /** + * The id of the radio item. + */ + id?: string; + /** + * The label of the radio item. + */ + label?: string; +} + +/** + * The RadioItem component is used to render a radio item. + */ +export const RadioItem = ({ + className, + wrapperProps, + labelProps, + indicatorProps, + label, + id, + ...props +}: RadioItemProps) => { + const { wrapper, label: labelClass, radio, indicator } = radioTheme(); + + // Generate a unique id for the radio item from the label if the id is not provided + const kebabCaseLabel = label?.toLowerCase().replace(/\s+/g, '-'); + const componentId = id ?? kebabCaseLabel; + + return ( +
+ + + + {label && ( + + )} +
+ ); +}; diff --git a/packages/frontend/src/components/shared/Radio/index.ts b/packages/frontend/src/components/shared/Radio/index.ts new file mode 100644 index 00000000..6d49f1a8 --- /dev/null +++ b/packages/frontend/src/components/shared/Radio/index.ts @@ -0,0 +1,2 @@ +export * from './Radio'; +export * from './RadioItem'; diff --git a/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts b/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts index 4667fe83..7c39b077 100644 --- a/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts +++ b/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts @@ -9,6 +9,8 @@ export const tabsTheme = tv({ // Horizontal – default 'px-1', 'pb-5', + 'cursor-default', + 'select-none', 'text-elements-low-em', 'border-b-2', 'border-transparent', diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index bfe02385..7e7f84db 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -18,6 +18,8 @@ import { renderTabs, renderVerticalTabs, } from './renders/tabs'; +import { RADIO_OPTIONS } from './renders/radio'; +import { Radio } from 'components/shared/Radio'; import { renderInlineNotificationWithDescriptions, renderInlineNotifications, @@ -28,6 +30,7 @@ import { renderTooltips } from './renders/tooltip'; const Page = () => { const [singleDate, setSingleDate] = useState(); const [dateRange, setDateRange] = useState(); + const [selectedRadio, setSelectedRadio] = useState(''); return (
@@ -160,6 +163,27 @@ const Page = () => {
+ {/* Radio */} +
+

Radio

+
+ + +
+
+ +
+ {/* Inline notification */}

Inline Notification

diff --git a/packages/frontend/src/pages/components/renders/radio.ts b/packages/frontend/src/pages/components/renders/radio.ts new file mode 100644 index 00000000..ff262f66 --- /dev/null +++ b/packages/frontend/src/pages/components/renders/radio.ts @@ -0,0 +1,16 @@ +import { RadioOption } from 'components/shared/Radio'; + +export const RADIO_OPTIONS: RadioOption[] = [ + { + label: 'Label 1', + value: '1', + }, + { + label: 'Label 2', + value: '2', + }, + { + label: 'Label 3', + value: '3', + }, +]; diff --git a/yarn.lock b/yarn.lock index f4d59093..b789155b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3439,6 +3439,23 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-slot" "1.0.2" +"@radix-ui/react-radio-group@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz#3197f5dcce143bcbf961471bf89320735c0212d3" + integrity sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-roving-focus" "1.0.4" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-previous" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/react-roving-focus@1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974" From 7d1810ebd9b07650c6bbda69a1262aea04d63322 Mon Sep 17 00:00:00 2001 From: Wahyu Kurniawan Date: Thu, 22 Feb 2024 17:42:13 +0700 Subject: [PATCH 12/12] [T-4866: feat] Switch component (#92) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⚡️ feat: create switch component * 📝 docs: add switch to the example page * 🔧 chore: install `@radix-ui/react-switch` * 🎨 style: add inset shadow * 🎨 style: addjust input outline when error and focus --- packages/frontend/package.json | 1 + .../components/shared/Input/Input.theme.ts | 1 + .../components/shared/Switch/Switch.theme.ts | 84 ++++++++++++++++++ .../src/components/shared/Switch/Switch.tsx | 85 +++++++++++++++++++ .../src/components/shared/Switch/index.ts | 1 + .../frontend/src/pages/components/index.tsx | 24 ++++++ packages/frontend/tailwind.config.js | 1 + yarn.lock | 14 +++ 8 files changed, 211 insertions(+) create mode 100644 packages/frontend/src/components/shared/Switch/Switch.theme.ts create mode 100644 packages/frontend/src/components/shared/Switch/Switch.tsx create mode 100644 packages/frontend/src/components/shared/Switch/index.ts diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 2e040901..131ada73 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -7,6 +7,7 @@ "@material-tailwind/react": "^2.1.7", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", diff --git a/packages/frontend/src/components/shared/Input/Input.theme.ts b/packages/frontend/src/components/shared/Input/Input.theme.ts index 8def6a0d..bf5ce915 100644 --- a/packages/frontend/src/components/shared/Input/Input.theme.ts +++ b/packages/frontend/src/components/shared/Input/Input.theme.ts @@ -50,6 +50,7 @@ export const inputTheme = tv( 'outline-offset-0', 'outline-border-danger', 'shadow-none', + 'focus:outline-border-danger', ], helperText: 'text-elements-danger', }, diff --git a/packages/frontend/src/components/shared/Switch/Switch.theme.ts b/packages/frontend/src/components/shared/Switch/Switch.theme.ts new file mode 100644 index 00000000..87fbd092 --- /dev/null +++ b/packages/frontend/src/components/shared/Switch/Switch.theme.ts @@ -0,0 +1,84 @@ +import { tv, type VariantProps } from 'tailwind-variants'; + +export const switchTheme = tv({ + slots: { + wrapper: ['flex', 'items-start', 'gap-4', 'w-[375px]'], + switch: [ + 'h-6', + 'w-12', + 'rounded-full', + 'transition-all', + 'duration-500', + 'relative', + 'cursor-default', + 'shadow-inset', + 'focus-ring', + 'outline-none', + ], + thumb: [ + 'block', + 'h-4', + 'w-4', + 'translate-x-1', + 'transition-transform', + 'duration-100', + 'will-change-transform', + 'rounded-full', + 'shadow-button', + 'data-[state=checked]:translate-x-7', + 'bg-controls-elevated', + ], + label: [ + 'flex', + 'flex-1', + 'flex-col', + 'px-1', + 'gap-1', + 'text-sm', + 'text-elements-high-em', + 'tracking-[-0.006em]', + ], + description: ['text-xs', 'text-elements-low-em'], + }, + variants: { + checked: { + true: { + switch: [ + 'bg-controls-primary', + 'hover:bg-controls-primary-hovered', + 'focus-visible:bg-controls-primary-hovered', + ], + }, + false: { + switch: [ + 'bg-controls-inset', + 'hover:bg-controls-inset-hovered', + 'focus-visible:bg-controls-inset-hovered', + ], + }, + }, + disabled: { + true: { + switch: ['bg-controls-disabled', 'cursor-not-allowed'], + thumb: ['bg-elements-on-disabled'], + }, + }, + fullWidth: { + true: { + wrapper: ['w-full', 'justify-between'], + }, + }, + }, + compoundVariants: [ + { + checked: true, + disabled: true, + class: { + switch: ['bg-controls-disabled-active'], + thumb: ['bg-snowball-900'], + }, + }, + ], +}); + +export type SwitchVariants = VariantProps; diff --git a/packages/frontend/src/components/shared/Switch/Switch.tsx b/packages/frontend/src/components/shared/Switch/Switch.tsx new file mode 100644 index 00000000..32d35957 --- /dev/null +++ b/packages/frontend/src/components/shared/Switch/Switch.tsx @@ -0,0 +1,85 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; +import { type SwitchProps as SwitchRadixProps } from '@radix-ui/react-switch'; +import * as SwitchRadix from '@radix-ui/react-switch'; + +import { switchTheme, type SwitchVariants } from './Switch.theme'; + +interface SwitchProps + extends Omit, + SwitchVariants { + /** + * The label of the switch. + */ + label?: string; + /** + * The description of the switch. + */ + description?: string; + /** + * Custom wrapper props for the switch. + */ + wrapperProps?: ComponentPropsWithoutRef<'div'>; + /** + * Function that is called when the checked state of the switch changes. + * @param checked The new checked state of the switch. + */ + onCheckedChange?(checked: boolean): void; +} + +/** + * A switch is a component used for toggling between two states. + */ +export const Switch = ({ + className, + checked, + label, + description, + disabled, + name, + wrapperProps, + fullWidth, + ...props +}: SwitchProps) => { + const { + wrapper, + switch: switchClass, + thumb, + label: labelClass, + description: descriptionClass, + } = switchTheme({ + checked, + disabled, + fullWidth, + }); + + const switchComponent = ( + + + + ); + + // If a label is provided, wrap the switch in a label element. + if (label) { + return ( +
+ + {switchComponent} +
+ ); + } + + return switchComponent; +}; diff --git a/packages/frontend/src/components/shared/Switch/index.ts b/packages/frontend/src/components/shared/Switch/index.ts new file mode 100644 index 00000000..1b19c1d3 --- /dev/null +++ b/packages/frontend/src/components/shared/Switch/index.ts @@ -0,0 +1 @@ +export * from './Switch'; diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index 7e7f84db..c3643c24 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -18,6 +18,7 @@ import { renderTabs, renderVerticalTabs, } from './renders/tabs'; +import { Switch } from 'components/shared/Switch'; import { RADIO_OPTIONS } from './renders/radio'; import { Radio } from 'components/shared/Radio'; import { @@ -30,6 +31,7 @@ import { renderTooltips } from './renders/tooltip'; const Page = () => { const [singleDate, setSingleDate] = useState(); const [dateRange, setDateRange] = useState(); + const [switchValue, setSwitchValue] = useState(false); const [selectedRadio, setSelectedRadio] = useState(''); return ( @@ -163,6 +165,28 @@ const Page = () => {
+ {/* Switch */} +
+

Switch

+
+ + + + +
+
+ +
+ {/* Radio */}

Radio

diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js index 9a814026..cce3380a 100644 --- a/packages/frontend/tailwind.config.js +++ b/packages/frontend/tailwind.config.js @@ -154,6 +154,7 @@ export default withMT({ calendar: '0px 3px 20px rgba(8, 47, 86, 0.1), 0px 0px 4px rgba(8, 47, 86, 0.14)', field: '0px 1px 2px rgba(0, 0, 0, 0.04)', + inset: 'inset 0px 1px 0px rgba(8, 47, 86, 0.06)', }, spacing: { 2.5: '0.625rem', diff --git a/yarn.lock b/yarn.lock index b789155b..f6800333 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3480,6 +3480,20 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" +"@radix-ui/react-switch@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e" + integrity sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-previous" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/react-tabs@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2"