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 <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-01-29 18:18:43 +05:30 committed by Ashwin Phatak
parent 8c38d4788e
commit a34e2286a6
15 changed files with 239 additions and 40 deletions

View File

@ -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<boolean> {
async redeployToProdById (deploymentId: string): Promise<Deployment> {
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<boolean> {
@ -317,4 +313,56 @@ export class Database {
return false;
}
}
async addDomainByProjectId (projectId: string, domainDetails: { name: string }): Promise<Domain[]> {
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<Domain>);
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<Domain>);
const savedRedirectedDomain = await domainRepository.save(redirectedDomain);
return [savedPrimaryDomain, savedRedirectedDomain];
}
async getDomainsByProjectId (projectId: string): Promise<Domain[]> {
const domainRepository = this.dataSource.getRepository(Domain);
const domains = await domainRepository.find({
where: {
project: {
id: projectId
}
}
});
return domains;
}
}

View File

@ -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;

View File

@ -98,6 +98,15 @@ export const createResolvers = async (db: Database): Promise<any> => {
});
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<any> => {
}
},
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<any> => {
}
},
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<any> => {
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;
}
}
}
};

View File

@ -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!
}

View File

@ -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"
}
]

View File

@ -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,

View File

@ -56,7 +56,6 @@ const loadAndSaveData = async <Entity extends ObjectLiteral>(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

View File

@ -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,

View File

@ -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<Domain[]>([]);
const { projects } = useOutletContext<ProjectSearchOutletContext>();
@ -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 = () => {
</Link>
</div>
{domains?.map((domain) => {
{domains.map((domain) => {
return (
<DomainCard
domain={domain}

View File

@ -47,7 +47,9 @@ const SetupDomain = () => {
return (
<form
onSubmit={handleSubmit(() => {
navigate('config');
watch('isWWW') === 'true'
? navigate(`config?name=www.${domainStr}`)
: navigate(`config?name=${domainStr}`);
})}
className="flex flex-col gap-6 w-full"
>

View File

@ -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 (
<div className="flex flex-col gap-6 w-full">
@ -43,7 +72,11 @@ const Config = () => {
</Alert>
<Link to={`/projects/${id}`}>
<Button className="w-fit" color="blue">
<Button
className="w-fit"
color="blue"
onClick={async () => await handleSubmitDomain()}
>
Finish <i>{'>'}</i>
</Button>
</Link>

View File

@ -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<AddDomainResponse> {
const { data } = await this.client.mutate({
mutation: addDomain,
variables: {
projectId,
domainDetails
}
});
return data;
}
async getDomains (projectId: string): Promise<GetDomainsResponse> {
const { data } = await this.client.query({
query: getDomains,
variables: {
projectId
}
});
return data;
}
}

View File

@ -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)
}
`;

View File

@ -215,3 +215,17 @@ query ($searchText: String!) {
}
}
`;
export const getDomains = gql`
query ($projectId: String!) {
domains(projectId: $projectId) {
branch
createdAt
isRedirected
id
name
status
updatedAt
}
}
`;

View File

@ -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
}