From a34e2286a61cd55829c52a8a9ac420ad9e3a26c4 Mon Sep 17 00:00:00 2001 From: Nabarun Gogoi Date: Mon, 29 Jan 2024 18:18:43 +0530 Subject: [PATCH] Implement functionality for adding new domain in project settings (#48) * Add mutation to create new domain * Update mutation to create two domains for www and non-www variant * Add gql-client method and frontend to create domain * Display fetched domains in domains tab panel using project id * Fix graphql type for fetching domains --------- Co-authored-by: neeraj --- packages/backend/src/database.ts | 62 ++++++++++++++++--- packages/backend/src/entity/Domain.ts | 15 ++++- packages/backend/src/resolvers.ts | 26 +++++++- packages/backend/src/schema.gql | 6 ++ .../backend/test/fixtures/deployments.json | 18 +++--- packages/backend/test/fixtures/domains.json | 15 ++++- packages/backend/test/initialize-db.ts | 7 ++- .../projects/project/settings/DomainCard.tsx | 2 +- .../projects/project/settings/Domains.tsx | 26 +++++--- .../projects/project/settings/SetupDomain.tsx | 4 +- .../pages/projects/id/domain/add/Config.tsx | 37 ++++++++++- packages/gql-client/src/client.ts | 29 ++++++++- packages/gql-client/src/mutations.ts | 6 ++ packages/gql-client/src/queries.ts | 14 +++++ packages/gql-client/src/types.ts | 12 ++++ 15 files changed, 239 insertions(+), 40 deletions(-) diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index 84ed518e..b471e867 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -11,6 +11,7 @@ import { Project } from './entity/Project'; import { Deployment, Environment } from './entity/Deployment'; import { ProjectMember } from './entity/ProjectMember'; import { EnvironmentVariable } from './entity/EnvironmentVariable'; +import { Domain } from './entity/Domain'; const log = debug('snowball:database'); @@ -261,7 +262,7 @@ export class Database { } } - async redeployToProdById (deploymentId: string): Promise { + async redeployToProdById (deploymentId: string): Promise { const deploymentRepository = this.dataSource.getRepository(Deployment); const deployment = await deploymentRepository.findOne({ relations: { @@ -284,13 +285,8 @@ export class Database { } await deploymentRepository.update({ id: Number(deploymentId) }, { domain: null, isCurrent: false }); - const savedUpdatedDeployment = await deploymentRepository.save(updatedDeployment); - if (savedUpdatedDeployment) { - return true; - } else { - return false; - } + return deploymentRepository.save(updatedDeployment); } async deleteProjectById (projectId: string): Promise { @@ -317,4 +313,56 @@ export class Database { return false; } } + + async addDomainByProjectId (projectId: string, domainDetails: { name: string }): Promise { + const domainRepository = this.dataSource.getRepository(Domain); + const projectRepository = this.dataSource.getRepository(Project); + + const currentProject = await projectRepository.findOneBy({ + id: projectId + }); + + if (currentProject === null) { + throw new Error(`Project with ${projectId} not found`); + } + + const primaryDomainDetails = { + ...domainDetails, + isRedirected: false, + branch: currentProject.prodBranch, + project: currentProject + }; + + const primaryDomain = domainRepository.create(primaryDomainDetails as DeepPartial); + + const domainArr = domainDetails.name.split('www.'); + + const redirectedDomainDetails = { + name: domainArr.length > 1 ? domainArr[1] : `www.${domainArr[0]}`, + isRedirected: true, + branch: currentProject.prodBranch, + project: currentProject + }; + + const savedPrimaryDomain = await domainRepository.save(primaryDomain); + + const redirectedDomain = domainRepository.create(redirectedDomainDetails as DeepPartial); + const savedRedirectedDomain = await domainRepository.save(redirectedDomain); + + return [savedPrimaryDomain, savedRedirectedDomain]; + } + + async getDomainsByProjectId (projectId: string): Promise { + const domainRepository = this.dataSource.getRepository(Domain); + + const domains = await domainRepository.find({ + where: { + project: { + id: projectId + } + } + }); + + return domains; + } } diff --git a/packages/backend/src/entity/Domain.ts b/packages/backend/src/entity/Domain.ts index 63d5a4e6..e72854ab 100644 --- a/packages/backend/src/entity/Domain.ts +++ b/packages/backend/src/entity/Domain.ts @@ -3,10 +3,14 @@ import { PrimaryGeneratedColumn, Column, CreateDateColumn, - UpdateDateColumn + UpdateDateColumn, + ManyToOne, + JoinColumn } from 'typeorm'; -enum Status { +import { Project } from './Project'; + +export enum Status { Live = 'Live', Pending = 'Pending', } @@ -16,6 +20,10 @@ export class Domain { @PrimaryGeneratedColumn() id!: number; + @ManyToOne(() => Project, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'projectId' }) + project!: Project; + @Column('varchar', { length: 255, default: 'main' }) branch!: string; @@ -26,7 +34,8 @@ export class Domain { isRedirected!: boolean; @Column({ - enum: Status + enum: Status, + default: Status.Pending }) status!: Status; diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index 8d99c3c8..a2e9f318 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -98,6 +98,15 @@ export const createResolvers = async (db: Database): Promise => { }); return projects; + }, + + domains: async (_:any, { projectId }: { projectId: string }) => { + try { + return db.getDomainsByProjectId(projectId); + } catch (err) { + log(err); + return false; + } } }, @@ -133,7 +142,7 @@ export const createResolvers = async (db: Database): Promise => { } }, - updateDeploymentToProd: async (_: any, { deploymentId }: {deploymentId: string }) => { + updateDeploymentToProd: async (_: any, { deploymentId }: { deploymentId: string }) => { try { return db.updateDeploymentById(deploymentId, { environment: Environment.Production @@ -153,9 +162,10 @@ export const createResolvers = async (db: Database): Promise => { } }, - redeployToProd: async (_: any, { deploymentId }: {deploymentId: string }) => { + redeployToProd: async (_: any, { deploymentId }: { deploymentId: string }) => { try { - return db.redeployToProdById(deploymentId); + await db.redeployToProdById(deploymentId); + return true; } catch (err) { log(err); return false; @@ -178,6 +188,16 @@ export const createResolvers = async (db: Database): Promise => { log(err); return false; } + }, + + addDomain: async (_: any, { projectId, domainDetails }: { projectId: string, domainDetails: { name: string } }) => { + try { + await db.addDomainByProjectId(projectId, domainDetails); + return true; + } catch (err) { + log(err); + return false; + } } } }; diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index c07d5289..c8c27b20 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -122,6 +122,7 @@ type Query { environmentVariables(projectId: String!): [EnvironmentVariable!] projectMembers(projectId: String!): [ProjectMember!] searchProjects(searchText: String!): [Project!] + domains(projectId: String!): [Domain!] } type Mutation { @@ -132,6 +133,7 @@ type Mutation { redeployToProd(deploymentId: String!): Boolean! deleteProject(projectId: String!): Boolean! rollbackDeployment(projectId: String!, deploymentId: String!): Boolean! + addDomain(projectId: String!, domainDetails: AddDomainInput!): Boolean! } input AddEnvironmentVariableInput { @@ -144,3 +146,7 @@ input UpdateProjectInput { name: String description: String } + +input AddDomainInput { + name: String! +} diff --git a/packages/backend/test/fixtures/deployments.json b/packages/backend/test/fixtures/deployments.json index 9ae78457..494b1e65 100644 --- a/packages/backend/test/fixtures/deployments.json +++ b/packages/backend/test/fixtures/deployments.json @@ -6,7 +6,7 @@ "status": "Building", "environment": "Production", "isCurrent": true, - "branch": "prod", + "branch": "main", "commitHash": "testXyz" }, { @@ -16,7 +16,7 @@ "status": "Ready", "environment": "Preview", "isCurrent": false, - "branch": "prod", + "branch": "test", "commitHash": "testXyz" }, { @@ -26,7 +26,7 @@ "status": "Error", "environment": "Development", "isCurrent": false, - "branch": "prod", + "branch": "test", "commitHash": "testXyz" }, { @@ -46,7 +46,7 @@ "status": "Building", "environment": "Production", "isCurrent": true, - "branch": "prod", + "branch": "main", "commitHash": "testXyz" }, { @@ -56,7 +56,7 @@ "status": "Ready", "environment": "Preview", "isCurrent": false, - "branch": "prod", + "branch": "test", "commitHash": "testXyz" }, { @@ -66,7 +66,7 @@ "status": "Error", "environment": "Development", "isCurrent": false, - "branch": "prod", + "branch": "test", "commitHash": "testXyz" }, { @@ -76,7 +76,7 @@ "status": "Building", "environment": "Production", "isCurrent": true, - "branch": "prod", + "branch": "main", "commitHash": "testXyz" }, { @@ -86,7 +86,7 @@ "status": "Ready", "environment": "Preview", "isCurrent": false, - "branch": "prod", + "branch": "test", "commitHash": "testXyz" }, { @@ -96,7 +96,7 @@ "status": "Error", "environment": "Development", "isCurrent": false, - "branch": "prod", + "branch": "test", "commitHash": "testXyz" } ] diff --git a/packages/backend/test/fixtures/domains.json b/packages/backend/test/fixtures/domains.json index 0631d7cb..63fd462c 100644 --- a/packages/backend/test/fixtures/domains.json +++ b/packages/backend/test/fixtures/domains.json @@ -1,53 +1,62 @@ [ { + "projectIndex": 0, "name": "randomurl.snowballtools.xyz", "status": "Live", "isRedirected": false, - "branch": "test" + "branch": "main" }, { + "projectIndex": 0, "name": "saugatt.com", "status": "Pending", "isRedirected": false, "branch": "test" }, { + "projectIndex": 0, "name": "www.saugatt.com", "status": "Pending", "isRedirected": true, "branch": "test" }, { + "projectIndex": 1, "name": "randomurl.snowballtools.xyz", "status": "Live", "isRedirected": false, - "branch": "test" + "branch": "main" }, { + "projectIndex": 1, "name": "saugatt.com", "status": "Pending", "isRedirected": false, "branch": "test" }, { + "projectIndex": 1, "name": "www.saugatt.com", "status": "Pending", "isRedirected": true, "branch": "test" }, { + "projectIndex": 2, "name": "randomurl.snowballtools.xyz", "status": "Live", "isRedirected": false, - "branch": "test" + "branch": "main" }, { + "projectIndex": 2, "name": "saugatt.com", "status": "Pending", "isRedirected": false, "branch": "test" }, { + "projectIndex": 2, "name": "www.saugatt.com", "status": "Pending", "isRedirected": true, diff --git a/packages/backend/test/initialize-db.ts b/packages/backend/test/initialize-db.ts index ee647ca9..2c3e075a 100644 --- a/packages/backend/test/initialize-db.ts +++ b/packages/backend/test/initialize-db.ts @@ -56,7 +56,6 @@ const loadAndSaveData = async (entityType: EntityT 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 savedDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, DOMAIN_DATA_PATH)); const projectRelations = { owner: savedUsers, @@ -65,6 +64,12 @@ const generateTestData = async (dataSource: DataSource) => { const savedProjects = await loadAndSaveData(Project, dataSource, path.resolve(__dirname, PROJECT_DATA_PATH), projectRelations); + const domainRelations = { + project: savedProjects + }; + + const savedDomains = await loadAndSaveData(Domain, dataSource, path.resolve(__dirname, DOMAIN_DATA_PATH), domainRelations); + const userOrganizationRelations = { member: savedUsers, organization: savedOrgs diff --git a/packages/frontend/src/components/projects/project/settings/DomainCard.tsx b/packages/frontend/src/components/projects/project/settings/DomainCard.tsx index 088312ee..02cfe568 100644 --- a/packages/frontend/src/components/projects/project/settings/DomainCard.tsx +++ b/packages/frontend/src/components/projects/project/settings/DomainCard.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import toast from 'react-hot-toast'; +import { Domain, DomainStatus } from 'gql-client'; import { Chip, @@ -14,7 +15,6 @@ import { import { ProjectDetails, RepositoryDetails } from '../../../../types/project'; import ConfirmDialog from '../../../shared/ConfirmDialog'; import EditDomainDialog from './EditDomainDialog'; -import { Domain, DomainStatus } from 'gql-client'; enum RefreshStatus { IDLE, diff --git a/packages/frontend/src/components/projects/project/settings/Domains.tsx b/packages/frontend/src/components/projects/project/settings/Domains.tsx index 79a58114..3ceb88dd 100644 --- a/packages/frontend/src/components/projects/project/settings/Domains.tsx +++ b/packages/frontend/src/components/projects/project/settings/Domains.tsx @@ -1,13 +1,18 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useParams, Link, useOutletContext } from 'react-router-dom'; import { Button, Typography } from '@material-tailwind/react'; import DomainCard from './DomainCard'; import { ProjectSearchOutletContext } from '../../../../types/project'; +import { useGQLClient } from '../../../../context/GQLClientContext'; +import { Domain } from 'gql-client'; const Domains = () => { const { id } = useParams(); + const client = useGQLClient(); + + const [domains, setDomains] = useState([]); const { projects } = useOutletContext(); @@ -23,11 +28,18 @@ const Domains = () => { ); }, [currentProject]); - const domains = currentProject?.deployments - .filter((deployment) => { - return deployment.domain != null; - }) - .map((deployment) => deployment.domain); + const fetchDomains = async () => { + if (currentProject === undefined) { + return; + } + + const fetchedDomains = await client.getDomains(currentProject.id); + setDomains(fetchedDomains.domains); + }; + + useEffect(() => { + fetchDomains(); + }, []); return ( <> @@ -40,7 +52,7 @@ const Domains = () => { - {domains?.map((domain) => { + {domains.map((domain) => { return ( { return (
{ - navigate('config'); + watch('isWWW') === 'true' + ? navigate(`config?name=www.${domainStr}`) + : navigate(`config?name=${domainStr}`); })} className="flex flex-col gap-6 w-full" > diff --git a/packages/frontend/src/pages/projects/id/domain/add/Config.tsx b/packages/frontend/src/pages/projects/id/domain/add/Config.tsx index 6d3be8a3..ac9db74a 100644 --- a/packages/frontend/src/pages/projects/id/domain/add/Config.tsx +++ b/packages/frontend/src/pages/projects/id/domain/add/Config.tsx @@ -1,9 +1,38 @@ import React from 'react'; -import { Link, useParams } from 'react-router-dom'; + +import toast from 'react-hot-toast'; +import { Link, useParams, useSearchParams } from 'react-router-dom'; import { Typography, Alert, Button } from '@material-tailwind/react'; +import { useGQLClient } from '../../../../../context/GQLClientContext'; + const Config = () => { const { id } = useParams(); + const client = useGQLClient(); + const [searchParams] = useSearchParams(); + const primaryDomainName = searchParams.get('name'); + + const handleSubmitDomain = async () => { + if (primaryDomainName === null) { + toast.error('Cannot resolve domain name'); + return; + } + + if (id === undefined) { + toast.error('Cannot find project'); + return; + } + + const isAdded = await client.addDomain(id, { + name: primaryDomainName, + }); + + if (isAdded) { + toast.success('Domain added successfully'); + } else { + toast.error('Error adding domain'); + } + }; return (
@@ -43,7 +72,11 @@ const Config = () => { - diff --git a/packages/gql-client/src/client.ts b/packages/gql-client/src/client.ts index 253ede34..ade2cab2 100644 --- a/packages/gql-client/src/client.ts +++ b/packages/gql-client/src/client.ts @@ -1,8 +1,8 @@ import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; -import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getProjectsInOrganization } from './queries'; -import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse } from './types'; -import { removeMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, rollbackDeployment } from './mutations'; +import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getDomains, getProjectsInOrganization } from './queries'; +import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse, AddDomainInput, AddDomainResponse, GetDomainsResponse } from './types'; +import { removeMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment } from './mutations'; export interface GraphQLConfig { gqlEndpoint: string; @@ -192,4 +192,27 @@ export class GQLClient { return data; } + + async addDomain (projectId: string, domainDetails: AddDomainInput): Promise { + const { data } = await this.client.mutate({ + mutation: addDomain, + variables: { + projectId, + domainDetails + } + }); + + return data; + } + + async getDomains (projectId: string): Promise { + const { data } = await this.client.query({ + query: getDomains, + variables: { + projectId + } + }); + + return data; + } } diff --git a/packages/gql-client/src/mutations.ts b/packages/gql-client/src/mutations.ts index a14db98a..f151d306 100644 --- a/packages/gql-client/src/mutations.ts +++ b/packages/gql-client/src/mutations.ts @@ -40,3 +40,9 @@ mutation ($projectId: String! ,$deploymentId: String!) { rollbackDeployment(proejctId: $projectId, deploymentId: $deploymentId) } `; + +export const addDomain = gql` +mutation ($projectId: String!, $domainDetails: AddDomainInput!) { + addDomain(projectId: $projectId, domainDetails: $domainDetails) +} +`; diff --git a/packages/gql-client/src/queries.ts b/packages/gql-client/src/queries.ts index 6df622fd..21590fa5 100644 --- a/packages/gql-client/src/queries.ts +++ b/packages/gql-client/src/queries.ts @@ -215,3 +215,17 @@ query ($searchText: String!) { } } `; + +export const getDomains = gql` +query ($projectId: String!) { + domains(projectId: $projectId) { + branch + createdAt + isRedirected + id + name + status + updatedAt + } +} +`; diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index 2baaa980..a2033b1e 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -161,6 +161,10 @@ export type GetProjectsInOrganizationResponse = { projectsInOrganization: Project[] } +export type GetDomainsResponse = { + domains: Domain[] +} + export type SearchProjectsResponse = { searchProjects: Project[] } @@ -199,3 +203,11 @@ export type RedeployToProdResponse = { export type RollbackDeploymentResponse = { rollbackDeployment: boolean } + +export type AddDomainInput = { + name: string +} + +export type AddDomainResponse = { + addDomain: true +}