diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index 1a48c35d..35c3aa65 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -33,7 +33,7 @@ export class Database { private dataSource: DataSource; private projectDomain: string; - constructor ({ dbPath } : DatabaseConfig, { projectDomain } : MiscConfig) { + constructor({ dbPath }: DatabaseConfig, { projectDomain }: MiscConfig) { this.dataSource = new DataSource({ type: 'better-sqlite3', database: dbPath, @@ -45,7 +45,7 @@ export class Database { this.projectDomain = projectDomain; } - async init (): Promise { + async init(): Promise { await this.dataSource.initialize(); log('database initialized'); @@ -58,21 +58,21 @@ export class Database { } } - async getUser (options: FindOneOptions): Promise { + async getUser(options: FindOneOptions): Promise { const userRepository = this.dataSource.getRepository(User); const user = await userRepository.findOne(options); return user; } - async addUser (data: DeepPartial): Promise { + async addUser(data: DeepPartial): Promise { const userRepository = this.dataSource.getRepository(User); const user = await userRepository.save(data); return user; } - async updateUser (user: User, data: DeepPartial): Promise { + async updateUser(user: User, data: DeepPartial): Promise { const userRepository = this.dataSource.getRepository(User); const updateResult = await userRepository.update({ id: user.id }, data); assert(updateResult.affected); @@ -80,7 +80,7 @@ export class Database { return updateResult.affected > 0; } - async getOrganizations ( + async getOrganizations( options: FindManyOptions ): Promise { const organizationRepository = this.dataSource.getRepository(Organization); @@ -89,7 +89,7 @@ export class Database { return organizations; } - async getOrganization ( + async getOrganization( options: FindOneOptions ): Promise { const organizationRepository = this.dataSource.getRepository(Organization); @@ -98,7 +98,7 @@ export class Database { return organization; } - async getOrganizationsByUserId (userId: string): Promise { + async getOrganizationsByUserId(userId: string): Promise { const organizationRepository = this.dataSource.getRepository(Organization); const userOrgs = await organizationRepository.find({ @@ -114,21 +114,21 @@ export class Database { return userOrgs; } - async addUserOrganization (data: DeepPartial): Promise { + async addUserOrganization(data: DeepPartial): Promise { const userOrganizationRepository = this.dataSource.getRepository(UserOrganization); const newUserOrganization = await userOrganizationRepository.save(data); return newUserOrganization; } - async getProjects (options: FindManyOptions): Promise { + async getProjects(options: FindManyOptions): Promise { const projectRepository = this.dataSource.getRepository(Project); const projects = await projectRepository.find(options); return projects; } - async getProjectById (projectId: string): Promise { + async getProjectById(projectId: string): Promise { const projectRepository = this.dataSource.getRepository(Project); const project = await projectRepository @@ -150,7 +150,20 @@ export class Database { return project; } - async getProjectsInOrganization ( + async allProjectsWithoutDeployments(): Promise { + const projectRepository = this.dataSource.getRepository(Project); + + const projects = await projectRepository + .createQueryBuilder('project') + .leftJoinAndSelect('project.deployments', 'deployment', 'deployment.deletedAt IS NULL') // Join only non-soft-deleted deployments + .where('deployment.id IS NULL') // Get projects where no deployments are present + .andWhere('project.auctionId IS NOT NULL') // Ensure auctionId is not null + .getMany(); + + return projects; + } + + async getProjectsInOrganization( userId: string, organizationSlug: string ): Promise { @@ -181,7 +194,7 @@ export class Database { /** * Get deployments with specified filter */ - async getDeployments ( + async getDeployments( options: FindManyOptions ): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); @@ -190,7 +203,7 @@ export class Database { return deployments; } - async getDeploymentsByProjectId (projectId: string): Promise { + async getDeploymentsByProjectId(projectId: string): Promise { return this.getDeployments({ relations: { project: true, @@ -208,7 +221,7 @@ export class Database { }); } - async getDeployment ( + async getDeployment( options: FindOneOptions ): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); @@ -217,14 +230,14 @@ export class Database { return deployment; } - async getDomains (options: FindManyOptions): Promise { + async getDomains(options: FindManyOptions): Promise { const domainRepository = this.dataSource.getRepository(Domain); const domains = await domainRepository.find(options); return domains; } - async addDeployment (data: DeepPartial): Promise { + async addDeployment(data: DeepPartial): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); const id = nanoid(); @@ -238,7 +251,7 @@ export class Database { return deployment; } - async getProjectMembersByProjectId ( + async getProjectMembersByProjectId( projectId: string ): Promise { const projectMemberRepository = @@ -259,7 +272,7 @@ export class Database { return projectMembers; } - async getEnvironmentVariablesByProjectId ( + async getEnvironmentVariablesByProjectId( projectId: string, filter?: FindOptionsWhere ): Promise { @@ -278,7 +291,7 @@ export class Database { return environmentVariables; } - async removeProjectMemberById (projectMemberId: string): Promise { + async removeProjectMemberById(projectMemberId: string): Promise { const projectMemberRepository = this.dataSource.getRepository(ProjectMember); @@ -293,7 +306,7 @@ export class Database { } } - async updateProjectMemberById ( + async updateProjectMemberById( projectMemberId: string, data: DeepPartial ): Promise { @@ -307,7 +320,7 @@ export class Database { return Boolean(updateResult.affected); } - async addProjectMember ( + async addProjectMember( data: DeepPartial ): Promise { const projectMemberRepository = @@ -317,7 +330,7 @@ export class Database { return newProjectMember; } - async addEnvironmentVariables ( + async addEnvironmentVariables( data: DeepPartial[] ): Promise { const environmentVariableRepository = @@ -328,7 +341,7 @@ export class Database { return savedEnvironmentVariables; } - async updateEnvironmentVariable ( + async updateEnvironmentVariable( environmentVariableId: string, data: DeepPartial ): Promise { @@ -342,7 +355,7 @@ export class Database { return Boolean(updateResult.affected); } - async deleteEnvironmentVariable ( + async deleteEnvironmentVariable( environmentVariableId: string ): Promise { const environmentVariableRepository = @@ -358,7 +371,7 @@ export class Database { } } - async getProjectMemberById (projectMemberId: string): Promise { + async getProjectMemberById(projectMemberId: string): Promise { const projectMemberRepository = this.dataSource.getRepository(ProjectMember); @@ -381,7 +394,7 @@ export class Database { return projectMemberWithProject[0]; } - async getProjectsBySearchText ( + async getProjectsBySearchText( userId: string, searchText: string ): Promise { @@ -403,14 +416,14 @@ export class Database { return projects; } - async updateDeploymentById ( + async updateDeploymentById( deploymentId: string, data: DeepPartial ): Promise { return this.updateDeployment({ id: deploymentId }, data); } - async updateDeployment ( + async updateDeployment( criteria: FindOptionsWhere, data: DeepPartial ): Promise { @@ -420,7 +433,7 @@ export class Database { return Boolean(updateResult.affected); } - async updateDeploymentsByProjectIds ( + async updateDeploymentsByProjectIds( projectIds: string[], data: DeepPartial ): Promise { @@ -436,7 +449,7 @@ export class Database { return Boolean(updateResult.affected); } - async deleteDeploymentById (deploymentId: string): Promise { + async deleteDeploymentById(deploymentId: string): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); const deployment = await deploymentRepository.findOneOrFail({ where: { @@ -449,7 +462,7 @@ export class Database { return Boolean(deleteResult); } - async addProject (user: User, organizationId: string, data: DeepPartial): Promise { + async addProject(user: User, organizationId: string, data: DeepPartial): Promise { const projectRepository = this.dataSource.getRepository(Project); // TODO: Check if organization exists @@ -468,7 +481,7 @@ export class Database { return projectRepository.save(newProject); } - async updateProjectById ( + async updateProjectById( projectId: string, data: DeepPartial ): Promise { @@ -481,7 +494,7 @@ export class Database { return Boolean(updateResult.affected); } - async deleteProjectById (projectId: string): Promise { + async deleteProjectById(projectId: string): Promise { const projectRepository = this.dataSource.getRepository(Project); const project = await projectRepository.findOneOrFail({ where: { @@ -497,7 +510,7 @@ export class Database { return Boolean(deleteResult); } - async deleteDomainById (domainId: string): Promise { + async deleteDomainById(domainId: string): Promise { const domainRepository = this.dataSource.getRepository(Domain); const deleteResult = await domainRepository.softDelete({ id: domainId }); @@ -509,21 +522,21 @@ export class Database { } } - async addDomain (data: DeepPartial): Promise { + async addDomain(data: DeepPartial): Promise { const domainRepository = this.dataSource.getRepository(Domain); const newDomain = await domainRepository.save(data); return newDomain; } - async getDomain (options: FindOneOptions): Promise { + async getDomain(options: FindOneOptions): Promise { const domainRepository = this.dataSource.getRepository(Domain); const domain = await domainRepository.findOne(options); return domain; } - async updateDomainById ( + async updateDomainById( domainId: string, data: DeepPartial ): Promise { @@ -533,7 +546,7 @@ export class Database { return Boolean(updateResult.affected); } - async getDomainsByProjectId ( + async getDomainsByProjectId( projectId: string, filter?: FindOptionsWhere ): Promise { diff --git a/packages/backend/src/registry.ts b/packages/backend/src/registry.ts index 45305c57..89f53ebc 100644 --- a/packages/backend/src/registry.ts +++ b/packages/backend/src/registry.ts @@ -416,14 +416,8 @@ export class Registry { }; } - async getCompletedAuctionIds(auctionIds: (string | null | undefined)[]): Promise { - const validAuctionIds = auctionIds.filter((id): id is string => id !== null && id !== undefined); - - if (!validAuctionIds.length) { - return null; - } - - const auctions = await this.registry.getAuctionsByIds(validAuctionIds); + async getCompletedAuctionIds(auctionIds: string[]): Promise { + const auctions = await this.registry.getAuctionsByIds(auctionIds); const completedAuctions = auctions .filter((auction: { id: string, status: string }) => auction.status === 'completed') diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index 5e640749..067a9f72 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -6,7 +6,7 @@ import { Permission } from './entity/ProjectMember'; import { Domain } from './entity/Domain'; import { Project } from './entity/Project'; import { EnvironmentVariable } from './entity/EnvironmentVariable'; -import { AddProjectFromTemplateInput, AuctionParams } from './types'; +import { AddProjectFromTemplateInput, AuctionParams, EnvironmentVariables } from './types'; const log = debug('snowball:resolver'); @@ -211,8 +211,15 @@ export const createResolvers = async (service: Service): Promise => { organizationSlug, data, lrn, - auctionParams - }: { organizationSlug: string; data: AddProjectFromTemplateInput; lrn: string; auctionParams: AuctionParams }, + auctionParams, + environmentVariables + }: { + organizationSlug: string; + data: AddProjectFromTemplateInput; + lrn: string; + auctionParams: AuctionParams, + environmentVariables: EnvironmentVariables[]; + }, context: any, ) => { try { @@ -221,7 +228,8 @@ export const createResolvers = async (service: Service): Promise => { organizationSlug, data, lrn, - auctionParams + auctionParams, + environmentVariables ); } catch (err) { log(err); @@ -235,12 +243,26 @@ export const createResolvers = async (service: Service): Promise => { organizationSlug, data, lrn, - auctionParams - }: { organizationSlug: string; data: DeepPartial; lrn: string; auctionParams: AuctionParams }, + auctionParams, + environmentVariables + }: { + organizationSlug: string; + data: DeepPartial; + lrn: string; + auctionParams: AuctionParams, + environmentVariables: EnvironmentVariables[]; + }, context: any, ) => { try { - return await service.addProject(context.user, organizationSlug, data, lrn, auctionParams); + return await service.addProject( + context.user, + organizationSlug, + data, + lrn, + auctionParams, + environmentVariables + ); } catch (err) { log(err); throw err; diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index df8bd03a..b4f74d72 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -271,12 +271,14 @@ type Mutation { data: AddProjectFromTemplateInput lrn: String auctionParams: AuctionParams + environmentVariables: [AddEnvironmentVariableInput!] ): Project! addProject( organizationSlug: String! data: AddProjectInput! lrn: String auctionParams: AuctionParams + environmentVariables: [AddEnvironmentVariableInput!] ): Project! updateProject(projectId: String!, data: UpdateProjectInput): Boolean! redeployToProd(deploymentId: String!): Boolean! diff --git a/packages/backend/src/service.ts b/packages/backend/src/service.ts index 728a067b..57ad9ef5 100644 --- a/packages/backend/src/service.ts +++ b/packages/backend/src/service.ts @@ -20,6 +20,7 @@ import { AppDeploymentRecord, AppDeploymentRemovalRecord, AuctionParams, + EnvironmentVariables, GitPushEventPayload, } from './types'; import { Role } from './entity/UserOrganization'; @@ -167,7 +168,7 @@ export class Service { async updateDeploymentsWithRecordData( records: AppDeploymentRecord[], ): Promise { - // get and update deployments to be updated using request id + // Fetch the deployments to be updated using deployment requestId const deployments = await this.db.getDeployments({ where: records.map((record) => ({ applicationDeploymentRequestId: record.attributes.request, @@ -220,10 +221,10 @@ export class Service { await Promise.all(deploymentUpdatePromises); - // if iscurrent is true for this deployment then update the old ones + // Get deployments that are in production environment const prodDeployments = Object.values(recordToDeploymentsMap).filter(deployment => deployment.isCurrent); - // Get deployment IDs of deployments that are in production environment + // Set the isCurrent state to false for the old deployments for (const deployment of prodDeployments) { const projectDeployments = await this.db.getDeploymentsByProjectId(deployment.projectId); const oldDeployments = projectDeployments @@ -236,15 +237,6 @@ export class Service { } } - // Get old deployments for ApplicationDeploymentRecords - // flter out deps with is current false - - // loop over these deps - // get the project - // get all the deployemnts in that proj with the same deployer lrn (query filter not above updated dep) - // set is current to false - - await Promise.all(deploymentUpdatePromises); } @@ -298,44 +290,30 @@ export class Service { * Calls the createDeploymentFromAuction method for deployments with completed auctions */ async checkAuctionStatus(): Promise { - const allProjects = await this.db.getProjects({ - where: { - auctionId: Not(IsNull()), - }, - relations: ['deployments'], - withDeleted: true, - }); + const projects = await this.db.allProjectsWithoutDeployments(); - // Should only check on the first deployment - const projects = allProjects.filter(project => { - if (project.deletedAt !== null) return false; + const validAuctionIds = projects.map((project) => project.auctionId!) + .filter((id): id is string => Boolean(id)); + const completedAuctionIds = await this.laconicRegistry.getCompletedAuctionIds(validAuctionIds); - return project.deployments.length === 0; - }); + const projectsToBedeployed = projects.filter((project) => + completedAuctionIds.includes(project.auctionId!) + ); - const auctionIds = projects.map((project) => project.auctionId); - const completedAuctionIds = await this.laconicRegistry.getCompletedAuctionIds(auctionIds); + for (const project of projectsToBedeployed) { + const deployerLrns = await this.laconicRegistry.getAuctionWinningDeployers(project!.auctionId!); - if (completedAuctionIds) { - const projectsToBedeployed = projects.filter((project) => - completedAuctionIds.includes(project.auctionId!) - ); + if (!deployerLrns) { + log(`No winning deployer for auction ${project!.auctionId}`); + } else { + // Update project with deployer LRNs + await this.db.updateProjectById(project.id!, { + deployerLrns + }); - for (const project of projectsToBedeployed) { - const deployerLrns = await this.laconicRegistry.getAuctionWinningDeployers(project!.auctionId!); - - if (!deployerLrns) { - log(`No winning deployer for auction ${project!.auctionId}`); - } else { - // Update project with deployer LRNs - await this.db.updateProjectById(project.id!, { - deployerLrns - }); - - for (const deployer of deployerLrns) { - log(`Creating deployment for deployer LRN ${deployer}`); - await this.createDeploymentFromAuction(project, deployer); - } + for (const deployer of deployerLrns) { + log(`Creating deployment for deployer LRN ${deployer}`); + await this.createDeploymentFromAuction(project, deployer); } } } @@ -785,7 +763,8 @@ export class Service { organizationSlug: string, data: AddProjectFromTemplateInput, lrn?: string, - auctionParams?: AuctionParams + auctionParams?: AuctionParams, + environmentVariables?: EnvironmentVariables[], ): Promise { try { const octokit = await this.getOctokit(user.id); @@ -816,7 +795,7 @@ export class Service { repository: gitRepo.data.full_name, // TODO: Set selected template template: 'webapp', - }, lrn, auctionParams); + }, lrn, auctionParams, environmentVariables); if (!project || !project.id) { throw new Error('Failed to create project from template'); @@ -834,7 +813,8 @@ export class Service { organizationSlug: string, data: DeepPartial, lrn?: string, - auctionParams?: AuctionParams + auctionParams?: AuctionParams, + environmentVariables?: EnvironmentVariables[], ): Promise { const organization = await this.db.getOrganization({ where: { @@ -846,7 +826,10 @@ export class Service { } const project = await this.db.addProject(user, organization.id, data); - log(`Project created ${project.id}`); + + if (environmentVariables) { + await this.addEnvironmentVariables(project.id, environmentVariables); + } const octokit = await this.getOctokit(user.id); const [owner, repo] = project.repository.split('/'); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 33cf0108..0b6a3a1b 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -76,3 +76,9 @@ export interface AuctionParams { maxPrice: string, numProviders: number, } + +export interface EnvironmentVariables { + environments: string[], + key: string, + value: string, +} diff --git a/packages/frontend/src/components/projects/create/Configure.tsx b/packages/frontend/src/components/projects/create/Configure.tsx index d1361788..7d81ee77 100644 --- a/packages/frontend/src/components/projects/create/Configure.tsx +++ b/packages/frontend/src/components/projects/create/Configure.tsx @@ -1,8 +1,9 @@ import { useCallback, useState } from 'react'; -import { useForm, Controller, SubmitHandler } from 'react-hook-form'; +import { useForm, Controller } from 'react-hook-form'; +import { FormProvider, FieldValues } from 'react-hook-form'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useMediaQuery } from 'usehooks-ts'; -import { AuctionParams } from 'gql-client'; +import { AddEnvironmentVariableInput, AuctionParams } from 'gql-client'; import { ArrowRightCircleFilledIcon, @@ -14,15 +15,20 @@ import { Select, SelectOption } from 'components/shared/Select'; import { Input } from 'components/shared/Input'; import { useToast } from 'components/shared/Toast'; import { useGQLClient } from '../../../context/GQLClientContext'; +import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm'; +import { EnvironmentVariablesFormValues } from 'types/types'; -type ConfigureFormValues = { +type ConfigureDeploymentFormValues = { option: string; lrn?: string; numProviders?: number; maxPrice?: string; }; +type ConfigureFormValues = ConfigureDeploymentFormValues & EnvironmentVariablesFormValues; + const Configure = () => { + const [isLoading, setIsLoading] = useState(false); const [searchParams] = useSearchParams(); const templateId = searchParams.get('templateId'); const queryParams = new URLSearchParams(location.search); @@ -40,96 +46,147 @@ const Configure = () => { const { toast, dismiss } = useToast(); const client = useGQLClient(); - const [isLoading, setIsLoading] = useState(false); - const { handleSubmit, control, watch } = useForm({ + const methods = useForm({ defaultValues: { option: 'LRN' }, }); - const selectedOption = watch('option'); + const selectedOption = methods.watch('option'); const isTabletView = useMediaQuery('(min-width: 720px)'); // md: const buttonSize = isTabletView ? { size: 'lg' as const } : {}; - const onSubmit: SubmitHandler = useCallback( - async (data) => { - setIsLoading(true); + const createProject = async (data: FieldValues, envVariables: AddEnvironmentVariableInput[]): Promise => { + setIsLoading(true); + let projectId: string | null = null; - try { - let lrn: string | undefined; - let auctionParams: AuctionParams | undefined; + try { + let lrn: string | undefined; + let auctionParams: AuctionParams | undefined; - if (data.option === 'LRN') { - lrn = data.lrn; - } else if (data.option === 'Auction') { - auctionParams = { - numProviders: Number(data.numProviders!), - maxPrice: (data.maxPrice!).toString(), - }; - } + if (data.option === 'LRN') { + lrn = data.lrn; + } else if (data.option === 'Auction') { + auctionParams = { + numProviders: Number(data.numProviders!), + maxPrice: (data.maxPrice!).toString(), + }; + } - if (templateId) { - // Template-based project creation - const projectData: any = { - templateOwner, - templateRepo, - owner, - name, - isPrivate, - }; + if (templateId) { + const projectData: any = { + templateOwner, + templateRepo, + owner, + name, + isPrivate, + }; - const { addProjectFromTemplate } = await client.addProjectFromTemplate( - orgSlug!, - projectData, - lrn, - auctionParams - ); + const { addProjectFromTemplate } = await client.addProjectFromTemplate( + orgSlug!, + projectData, + lrn, + auctionParams, + envVariables + ); - data.option === 'Auction' - ? navigate( - `/${orgSlug}/projects/create/success/${addProjectFromTemplate.id}?isAuction=true`, - ) - : navigate( - `/${orgSlug}/projects/create/template/deploy?projectId=${addProjectFromTemplate.id}&templateId=${templateId}` - ); - } else { - const { addProject } = await client.addProject( - orgSlug!, - { - name: fullName!, - prodBranch: defaultBranch!, - repository: fullName!, - template: 'webapp', - }, - lrn, - auctionParams - ); + projectId = addProjectFromTemplate.id; + } else { + const { addProject } = await client.addProject( + orgSlug!, + { + name: fullName!, + prodBranch: defaultBranch!, + repository: fullName!, + template: 'webapp', + }, + lrn, + auctionParams, + envVariables + ); - data.option === 'Auction' - ? navigate( - `/${orgSlug}/projects/create/success/${addProject.id}?isAuction=true` - ) - : navigate( - `/${orgSlug}/projects/create/deploy?projectId=${addProject.id}` - ); - } - } catch (error) { - console.error('Error creating project:', error); + projectId = addProject.id; + } + } catch (error) { + console.error('Error creating project:', error); + toast({ + id: 'error-creating-project', + title: 'Error creating project', + variant: 'error', + onDismiss: dismiss, + }); + } finally { + setIsLoading(false); + } + + if (projectId) { + return projectId; + } else { + throw new Error('Project creation failed'); + } + }; + + const handleFormSubmit = useCallback( + async (createFormData: FieldValues) => { + const environmentVariables = createFormData.variables.map((variable: any) => { + return { + key: variable.key, + value: variable.value, + environments: Object.entries(createFormData.environment) + .filter(([, value]) => value === true) + .map(([key]) => key.charAt(0).toUpperCase() + key.slice(1)), + }; + }); + + const projectId = await createProject(createFormData, environmentVariables); + + const { environmentVariables: isEnvironmentVariablesAdded } = + await client.getEnvironmentVariables(projectId); + + if (isEnvironmentVariablesAdded.length > 0) { toast({ - id: 'error-creating-project', - title: 'Error creating project', + id: + createFormData.variables.length > 1 + ? 'env_variable_added' + : 'env_variables_added', + title: + createFormData.variables.length > 1 + ? `${createFormData.variables.length} variables added` + : `Variable added`, + variant: 'success', + onDismiss: dismiss, + }); + } else { + toast({ + id: 'env_variables_not_added', + title: 'Environment variables not added', variant: 'error', onDismiss: dismiss, }); - } finally { - setIsLoading(false); + } + if (templateId) { + createFormData.option === 'Auction' + ? navigate( + `/${orgSlug}/projects/create/success/${projectId}?isAuction=true`, + ) + : navigate( + `/${orgSlug}/projects/create/template/deploy?projectId=${projectId}&templateId=${templateId}` + ); + } else { + createFormData.option === 'Auction' + ? navigate( + `/${orgSlug}/projects/create/success/${projectId}?isAuction=true` + ) + : navigate( + `/${orgSlug}/projects/create/deploy?projectId=${projectId}` + ); } }, - [client, isPrivate, templateId, navigate, dismiss, toast] + [client, createProject, dismiss, toast] ); return ( -
-
+
+
Configure deployment @@ -142,99 +199,111 @@ const Configure = () => {
-
-
-
- ( - + + )} />
-
- - Maximum Price (alnt) - - ( - - )} - /> -
- - )} + )} -
- -
-
-
+ {selectedOption === 'Auction' && ( + <> +
+ + Set the number of deployers and maximum price for each deployment + + + Number of Deployers + + ( + + )} + /> +
+
+ + Maximum Price (alnt) + + ( + + )} + /> +
+ + )} + + + Environment Variables + +
+ +
+ +
+ +
+ + +
); }; diff --git a/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx b/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx index 8f7c7811..aa51f0b2 100644 --- a/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx +++ b/packages/frontend/src/components/projects/create/RepositoryList/RepositoryList.tsx @@ -64,9 +64,9 @@ export const RepositoryList = () => { // Check if selected account is an organization if (selectedAccount.value === gitUser.login) { - query = query + ` user:${selectedAccount}`; + query = query + ` user:${selectedAccount.value}`; } else { - query = query + ` org:${selectedAccount}`; + query = query + ` org:${selectedAccount.value}`; } const result = await octokit.rest.search.repos({ diff --git a/packages/frontend/src/pages/org-slug/projects/create/template/index.tsx b/packages/frontend/src/pages/org-slug/projects/create/template/index.tsx index ebb9f3b9..4024e521 100644 --- a/packages/frontend/src/pages/org-slug/projects/create/template/index.tsx +++ b/packages/frontend/src/pages/org-slug/projects/create/template/index.tsx @@ -163,6 +163,7 @@ const CreateRepo = () => { ( )} @@ -172,7 +173,7 @@ const CreateRepo = () => { ( + render={({ }) => ( )} /> 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 a710c243..d284a4a6 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 @@ -1,22 +1,20 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useFieldArray, useForm } from 'react-hook-form'; +import { useCallback, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { Collapse, Checkbox } from '@snowballtools/material-tailwind-react-fork'; +import { Collapse } from '@snowballtools/material-tailwind-react-fork'; -import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow'; import DisplayEnvironmentVariables from 'components/projects/project/settings/DisplayEnvironmentVariables'; import { useGQLClient } from 'context/GQLClientContext'; import { EnvironmentVariablesFormValues } from '../../../../../types'; import HorizontalLine from 'components/HorizontalLine'; import { Heading } from 'components/shared/Heading'; -import { Button } from 'components/shared/Button'; -// import { Checkbox } from 'components/shared/Checkbox'; import { PlusIcon } from 'components/shared/CustomIcon'; -import { InlineNotification } from 'components/shared/InlineNotification'; import { ProjectSettingContainer } from 'components/projects/project/settings/ProjectSettingContainer'; import { useToast } from 'components/shared/Toast'; import { Environment, EnvironmentVariable } from 'gql-client'; +import EnvironmentVariablesForm from './EnvironmentVariablesForm'; +import { FieldValues, FormProvider, useForm } from 'react-hook-form'; +import { Button } from 'components/shared'; export const EnvironmentVariablesTabPanel = () => { const { id } = useParams(); @@ -27,13 +25,9 @@ export const EnvironmentVariablesTabPanel = () => { EnvironmentVariable[] >([]); - const { - handleSubmit, - register, - control, - reset, - formState: { isSubmitSuccessful, errors }, - } = useForm({ + const [createNewVariable, setCreateNewVariable] = useState(false); + + const methods = useForm({ defaultValues: { variables: [{ key: '', value: '' }], environment: { @@ -43,21 +37,6 @@ export const EnvironmentVariablesTabPanel = () => { }, }, }); - const [createNewVariable, setCreateNewVariable] = useState(false); - - const { fields, append, remove } = useFieldArray({ - name: 'variables', - control, - rules: { - required: 'Add at least 1 environment variables', - }, - }); - - useEffect(() => { - if (isSubmitSuccessful) { - reset(); - } - }, [isSubmitSuccessful, reset, id]); const getEnvironmentVariables = useCallback( (environment: Environment) => { @@ -68,21 +47,6 @@ export const EnvironmentVariablesTabPanel = () => { [environmentVariables, id], ); - const isFieldEmpty = useMemo(() => { - if (errors.variables) { - return fields.some((_, index) => { - if ( - errors.variables![index]?.value?.type === 'required' || - errors.variables![index]?.key?.type === 'required' - ) { - return true; - } - }); - } - - return false; - }, [fields, errors.variables, id]); - const fetchEnvironmentVariables = useCallback( async (id: string | undefined) => { if (id) { @@ -99,8 +63,8 @@ export const EnvironmentVariablesTabPanel = () => { }, [id]); const createEnvironmentVariablesHandler = useCallback( - async (createFormData: EnvironmentVariablesFormValues) => { - const environmentVariables = createFormData.variables.map((variable) => { + async (createFormData: FieldValues) => { + const environmentVariables = createFormData.variables.map((variable: any) => { return { key: variable.key, value: variable.value, @@ -114,7 +78,7 @@ export const EnvironmentVariablesTabPanel = () => { await client.addEnvironmentVariables(id!, environmentVariables); if (isEnvironmentVariablesAdded) { - reset(); + methods.reset(); setCreateNewVariable((cur) => !cur); fetchEnvironmentVariables(id); @@ -159,59 +123,10 @@ export const EnvironmentVariablesTabPanel = () => {
-
-
- {fields.map((field, index) => { - return ( - remove(index)} - isDeleteDisabled={fields.length === 1} - /> - ); - })} -
- - {/* TODO: Implement import environment varible functionality */} - -
- {isFieldEmpty && ( - - )} -
- - - + + createEnvironmentVariablesHandler(data))}> +
+
-
+
diff --git a/packages/frontend/src/pages/org-slug/projects/id/settings/EnvironmentVariablesForm.tsx b/packages/frontend/src/pages/org-slug/projects/id/settings/EnvironmentVariablesForm.tsx new file mode 100644 index 00000000..ebe9bdc2 --- /dev/null +++ b/packages/frontend/src/pages/org-slug/projects/id/settings/EnvironmentVariablesForm.tsx @@ -0,0 +1,76 @@ +import { useEffect, useMemo } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; + +// TODO: Use custom checkbox component +import { Checkbox } from '@snowballtools/material-tailwind-react-fork'; + +import { Button } from 'components/shared/Button'; +import { InlineNotification } from 'components/shared/InlineNotification'; +import AddEnvironmentVariableRow from 'components/projects/project/settings/AddEnvironmentVariableRow'; +import { EnvironmentVariablesFormValues } from 'types/types'; + +const EnvironmentVariablesForm = () => { + const { + register, + control, + reset, + formState: { isSubmitSuccessful, errors }, + } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + name: 'variables', + control, + }); + + useEffect(() => { + if (isSubmitSuccessful) { + reset(); + } + }, [isSubmitSuccessful, reset]); + + const isFieldEmpty = useMemo(() => { + if (errors.variables) { + return fields.some((_, index) => { + if ( + errors.variables![index]?.value?.type === 'required' || + errors.variables![index]?.key?.type === 'required' + ) { + return true; + } + }); + } + + return false; + }, [fields, errors.variables]); + + return ( + <> + {fields.map((field, index) => ( + remove(index)} + isDeleteDisabled={fields.length === 0} + /> + ))} +
+ +
+ {isFieldEmpty && ( + + )} +
+ + + +
+ + ); +}; + +export default EnvironmentVariablesForm; diff --git a/packages/gql-client/src/client.ts b/packages/gql-client/src/client.ts index f7099de5..0e0eff9f 100644 --- a/packages/gql-client/src/client.ts +++ b/packages/gql-client/src/client.ts @@ -233,6 +233,7 @@ export class GQLClient { data: types.AddProjectFromTemplateInput, lrn?: string, auctionParams?: types.AuctionParams, + environmentVariables?: types.AddEnvironmentVariableInput[] ): Promise { const result = await this.client.mutate({ mutation: mutations.addProjectFromTemplate, @@ -240,7 +241,8 @@ export class GQLClient { organizationSlug, data, lrn, - auctionParams + auctionParams, + environmentVariables }, }); @@ -252,6 +254,7 @@ export class GQLClient { data: types.AddProjectInput, lrn?: string, auctionParams?: types.AuctionParams, + environmentVariables?: types.AddEnvironmentVariableInput[] ): Promise { const result = await this.client.mutate({ mutation: mutations.addProject, @@ -259,7 +262,8 @@ export class GQLClient { organizationSlug, data, lrn, - auctionParams + auctionParams, + environmentVariables }, }); diff --git a/packages/gql-client/src/mutations.ts b/packages/gql-client/src/mutations.ts index 1ad04e22..876e5611 100644 --- a/packages/gql-client/src/mutations.ts +++ b/packages/gql-client/src/mutations.ts @@ -49,16 +49,16 @@ export const updateDeploymentToProd = gql` `; export const addProjectFromTemplate = gql` - mutation ($organizationSlug: String!, $data: AddProjectFromTemplateInput, $lrn: String, $auctionParams: AuctionParams) { - addProjectFromTemplate(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionParams: $auctionParams) { + mutation ($organizationSlug: String!, $data: AddProjectFromTemplateInput, $lrn: String, $auctionParams: AuctionParams, $environmentVariables: [AddEnvironmentVariableInput!]) { + addProjectFromTemplate(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionParams: $auctionParams, environmentVariables: $environmentVariables) { id } } `; export const addProject = gql` - mutation ($organizationSlug: String!, $data: AddProjectInput!, $lrn: String, $auctionParams: AuctionParams) { - addProject(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionParams: $auctionParams) { + mutation ($organizationSlug: String!, $data: AddProjectInput!, $lrn: String, $auctionParams: AuctionParams, $environmentVariables: [AddEnvironmentVariableInput!]) { + addProject(organizationSlug: $organizationSlug, data: $data, lrn: $lrn, auctionParams: $auctionParams, environmentVariables: $environmentVariables) { id } }