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:
parent
8c38d4788e
commit
a34e2286a6
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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!
|
||||
}
|
||||
|
18
packages/backend/test/fixtures/deployments.json
vendored
18
packages/backend/test/fixtures/deployments.json
vendored
@ -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"
|
||||
}
|
||||
]
|
||||
|
15
packages/backend/test/fixtures/domains.json
vendored
15
packages/backend/test/fixtures/domains.json
vendored
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
`;
|
||||
|
@ -215,3 +215,17 @@ query ($searchText: String!) {
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getDomains = gql`
|
||||
query ($projectId: String!) {
|
||||
domains(projectId: $projectId) {
|
||||
branch
|
||||
createdAt
|
||||
isRedirected
|
||||
id
|
||||
name
|
||||
status
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user