Integrate rollback deployment GQL mutation in frontend (#49)

* Use rollback deployment client in UI

* Check if deployements domain is undefined

* Fix typo

* Rename variable to current deployment

* Handle deployment domain relation on rollback

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-01-30 14:40:04 +05:30 committed by Ashwin Phatak
parent fdf06f9bd0
commit d97794f1bf
11 changed files with 84 additions and 72 deletions

View File

@ -304,8 +304,21 @@ export class Database {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
// TODO: Implement transactions // TODO: Implement transactions
const oldCurrentDeploymentUpdate = await deploymentRepository.update({ project: { id: projectId }, isCurrent: true }, { isCurrent: false }); const oldCurrentDeployment = await deploymentRepository.findOne({
const newCurrentDeploymentUpdate = await deploymentRepository.update({ id: Number(deploymentId) }, { isCurrent: true }); relations: {
domain: true
},
where: {
project: {
id: projectId
},
isCurrent: true
}
});
const oldCurrentDeploymentUpdate = await deploymentRepository.update({ project: { id: projectId }, isCurrent: true }, { isCurrent: false, domain: null });
const newCurrentDeploymentUpdate = await deploymentRepository.update({ id: Number(deploymentId) }, { isCurrent: true, domain: oldCurrentDeployment?.domain });
if (oldCurrentDeploymentUpdate.affected && newCurrentDeploymentUpdate.affected) { if (oldCurrentDeploymentUpdate.affected && newCurrentDeploymentUpdate.affected) {
return oldCurrentDeploymentUpdate.affected > 0 && newCurrentDeploymentUpdate.affected > 0; return oldCurrentDeploymentUpdate.affected > 0 && newCurrentDeploymentUpdate.affected > 0;

View File

@ -56,8 +56,8 @@ export const createResolvers = async (db: Database): Promise<any> => {
}, },
projectsInOrganization: async (_: any, { organizationId }: {organizationId: string }, context: any) => { projectsInOrganization: async (_: any, { organizationId }: {organizationId: string }, context: any) => {
const dbProject = await db.getProjectsInOrganization(context.userId, organizationId); const dbProjects = await db.getProjectsInOrganization(context.userId, organizationId);
return dbProject; return dbProjects;
}, },
deployments: async (_: any, { projectId }: { projectId: string }) => { deployments: async (_: any, { projectId }: { projectId: string }) => {

View File

@ -25,9 +25,8 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
<Link to={`projects/${project.id}`}> <Link to={`projects/${project.id}`}>
<Typography>{project.name}</Typography> <Typography>{project.name}</Typography>
<Typography color="gray" variant="small"> <Typography color="gray" variant="small">
{project.deployments[0]?.domain.name {project.deployments[0]?.domain?.name ??
? project.deployments[0]?.domain.name 'No Production Deployment'}
: ''}
</Typography> </Typography>
</Link> </Link>
</div> </div>

View File

@ -9,6 +9,7 @@ import FilterForm, {
} from './deployments/FilterForm'; } from './deployments/FilterForm';
import { DeploymentDetails } from '../../../types/project'; import { DeploymentDetails } from '../../../types/project';
import { useGQLClient } from '../../../context/GQLClientContext'; import { useGQLClient } from '../../../context/GQLClientContext';
import { COMMIT_DETAILS } from '../../../constants';
const DEFAULT_FILTER_VALUE: FilterValue = { const DEFAULT_FILTER_VALUE: FilterValue = {
searchedBranch: '', searchedBranch: '',
@ -23,25 +24,11 @@ const DeploymentsTabPanel = ({ projectId }: { projectId: string }) => {
const fetchDeployments = async () => { const fetchDeployments = async () => {
const { deployments } = await client.getDeployments(projectId); const { deployments } = await client.getDeployments(projectId);
const updatedDeployments = deployments.map((deployment: any) => { const updatedDeployments = deployments.map((deployment) => {
return { return {
...deployment, ...deployment,
isProduction: deployment.environment === 'Production',
author: '', author: '',
commit: { commit: COMMIT_DETAILS,
hash: '',
message: '',
},
domain: deployment.domain
? {
...deployment.domain,
record: {
type: '',
name: '',
value: '',
},
}
: null,
}; };
}); });
setDeployments(updatedDeployments); setDeployments(updatedDeployments);
@ -51,11 +38,11 @@ const DeploymentsTabPanel = ({ projectId }: { projectId: string }) => {
fetchDeployments(); fetchDeployments();
}, []); }, []);
const productionDeployment = useMemo(() => { const currentDeployment = useMemo(() => {
return deployments.find((deployment) => { return deployments.find((deployment) => {
return deployment.isProduction === true; return deployment.isCurrent === true;
}) as DeploymentDetails; });
}, []); }, [deployments]);
const filteredDeployments = useMemo<DeploymentDetails[]>(() => { const filteredDeployments = useMemo<DeploymentDetails[]>(() => {
return deployments.filter((deployment) => { return deployments.filter((deployment) => {
@ -102,8 +89,9 @@ const DeploymentsTabPanel = ({ projectId }: { projectId: string }) => {
<DeploymentDetailsCard <DeploymentDetailsCard
deployment={deployment} deployment={deployment}
key={key} key={key}
productionDeployment={productionDeployment} currentDeployment={currentDeployment!}
onUpdate={onUpdateDeploymenToProd} onUpdate={onUpdateDeploymenToProd}
projectId={projectId}
/> />
); );
}) })

View File

@ -31,9 +31,8 @@ const OverviewTabPanel = ({ project, organizationProject }: OverviewProps) => {
<div className="grow"> <div className="grow">
<Typography>{project.name}</Typography> <Typography>{project.name}</Typography>
<Typography variant="small" color="gray"> <Typography variant="small" color="gray">
{project.deployments[0]?.domain.name {project.deployments[0]?.domain?.name ??
? project.deployments[0]?.domain.name 'No Production Deployment'}
: ''}
</Typography> </Typography>
</div> </div>
</div> </div>

View File

@ -10,6 +10,7 @@ import {
ChipProps, ChipProps,
} from '@material-tailwind/react'; } from '@material-tailwind/react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { Environment } from 'gql-client';
import { relativeTimeMs } from '../../../../utils/time'; import { relativeTimeMs } from '../../../../utils/time';
import ConfirmDialog from '../../../shared/ConfirmDialog'; import ConfirmDialog from '../../../shared/ConfirmDialog';
@ -19,8 +20,9 @@ import { useGQLClient } from '../../../../context/GQLClientContext';
interface DeployDetailsCardProps { interface DeployDetailsCardProps {
deployment: DeploymentDetails; deployment: DeploymentDetails;
productionDeployment: DeploymentDetails; currentDeployment: DeploymentDetails;
onUpdate: () => Promise<void>; onUpdate: () => Promise<void>;
projectId: string;
} }
const STATUS_COLORS: { [key in Status]: ChipProps['color'] } = { const STATUS_COLORS: { [key in Status]: ChipProps['color'] } = {
@ -31,8 +33,9 @@ const STATUS_COLORS: { [key in Status]: ChipProps['color'] } = {
const DeploymentDetailsCard = ({ const DeploymentDetailsCard = ({
deployment, deployment,
productionDeployment, currentDeployment,
onUpdate, onUpdate,
projectId,
}: DeployDetailsCardProps) => { }: DeployDetailsCardProps) => {
const client = useGQLClient(); const client = useGQLClient();
@ -60,6 +63,19 @@ const DeploymentDetailsCard = ({
} }
}; };
const rollbackDeploymentHandler = async () => {
const isRollbacked = await client.rollbackDeployment(
projectId,
deployment.id,
);
if (isRollbacked) {
await onUpdate();
toast.success('Deployment rolled back');
} else {
toast.error('Unable to rollback deployment');
}
};
return ( return (
<div className="grid grid-cols-4 gap-2 border-b border-gray-300 p-3 my-2"> <div className="grid grid-cols-4 gap-2 border-b border-gray-300 p-3 my-2">
<div className="col-span-2"> <div className="col-span-2">
@ -73,7 +89,7 @@ const DeploymentDetailsCard = ({
/> />
</div> </div>
<Typography color="gray"> <Typography color="gray">
{deployment.isProduction {deployment.environment === Environment.Production
? `Production ${deployment.isCurrent ? '(Current)' : ''}` ? `Production ${deployment.isCurrent ? '(Current)' : ''}`
: 'Preview'} : 'Preview'}
</Typography> </Typography>
@ -81,7 +97,7 @@ const DeploymentDetailsCard = ({
<div className="col-span-1"> <div className="col-span-1">
<Typography color="gray">^ {deployment.branch}</Typography> <Typography color="gray">^ {deployment.branch}</Typography>
<Typography color="gray"> <Typography color="gray">
^ {deployment.commit.hash} {deployment.commit.message} ^ {deployment.commitHash} {deployment.commit.message}
</Typography> </Typography>
</div> </div>
<div className="col-span-1 flex items-center"> <div className="col-span-1 flex items-center">
@ -95,7 +111,7 @@ const DeploymentDetailsCard = ({
<MenuList> <MenuList>
<MenuItem>^ Visit</MenuItem> <MenuItem>^ Visit</MenuItem>
<MenuItem>^ Assign domain</MenuItem> <MenuItem>^ Assign domain</MenuItem>
{!deployment.isProduction && ( {!(deployment.environment === Environment.Production) && (
<MenuItem <MenuItem
onClick={() => setChangeToProduction(!changeToProduction)} onClick={() => setChangeToProduction(!changeToProduction)}
> >
@ -106,13 +122,19 @@ const DeploymentDetailsCard = ({
<hr className="my-3" /> <hr className="my-3" />
<MenuItem <MenuItem
onClick={() => setRedeployToProduction(!redeployToProduction)} onClick={() => setRedeployToProduction(!redeployToProduction)}
disabled={!(deployment.isProduction && deployment.isCurrent)} disabled={
!(
deployment.environment === Environment.Production &&
deployment.isCurrent
)
}
> >
^ Redeploy to production ^ Redeploy to production
</MenuItem> </MenuItem>
{!deployment.isProduction && ( {deployment.environment === Environment.Production && (
<MenuItem <MenuItem
onClick={() => setRollbackDeployment(!rollbackDeployment)} onClick={() => setRollbackDeployment(!rollbackDeployment)}
disabled={deployment.isCurrent}
> >
^ Rollback to this version ^ Rollback to this version
</MenuItem> </MenuItem>
@ -181,7 +203,10 @@ const DeploymentDetailsCard = ({
open={rollbackDeployment} open={rollbackDeployment}
confirmButtonTitle="Rollback" confirmButtonTitle="Rollback"
color="blue" color="blue"
handleConfirm={() => setRollbackDeployment((preVal) => !preVal)} handleConfirm={async () => {
await rollbackDeploymentHandler();
setRollbackDeployment((preVal) => !preVal);
}}
> >
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Typography variant="small"> <Typography variant="small">
@ -189,7 +214,7 @@ const DeploymentDetailsCard = ({
deployment deployment
</Typography> </Typography>
<DeploymentDialogBodyCard <DeploymentDialogBodyCard
deployment={productionDeployment} deployment={currentDeployment}
chip={{ chip={{
value: 'Live Deployment', value: 'Live Deployment',
color: 'green', color: 'green',
@ -206,10 +231,7 @@ const DeploymentDetailsCard = ({
These domains will point to your new deployment: These domains will point to your new deployment:
</Typography> </Typography>
<Typography variant="small" color="blue"> <Typography variant="small" color="blue">
^ saugatt.com ^ {currentDeployment.domain?.name}
</Typography>
<Typography variant="small" color="blue">
^ www.saugatt.com
</Typography> </Typography>
</div> </div>
</ConfirmDialog> </ConfirmDialog>

View File

@ -31,7 +31,7 @@ const DeploymentDialogBodyCard = ({
{deployment.title} {deployment.title}
</Typography> </Typography>
<Typography variant="small"> <Typography variant="small">
^ {deployment.branch} ^ {deployment.commit.hash}{' '} ^ {deployment.branch} ^ {deployment.commitHash}{' '}
{deployment.commit.message} {deployment.commit.message}
</Typography> </Typography>
<Typography variant="small"> <Typography variant="small">

View File

@ -0,0 +1,5 @@
export const COMMIT_DETAILS = {
message: 'subscription added',
createdAt: '2023-12-11T04:20:00',
branch: 'main',
};

View File

@ -6,6 +6,7 @@ import { Button, Typography, Chip } from '@material-tailwind/react';
import ProjectCard from '../components/projects/ProjectCard'; import ProjectCard from '../components/projects/ProjectCard';
import { useGQLClient } from '../context/GQLClientContext'; import { useGQLClient } from '../context/GQLClientContext';
import { ProjectDetails } from '../types/project'; import { ProjectDetails } from '../types/project';
import { COMMIT_DETAILS } from '../constants';
// TODO: Implement organization switcher // TODO: Implement organization switcher
const USER_ORGANIZATION_ID = '1'; const USER_ORGANIZATION_ID = '1';
@ -22,11 +23,7 @@ const Projects = () => {
return { return {
...project, ...project,
// TODO: Populate from github API // TODO: Populate from github API
latestCommit: { latestCommit: COMMIT_DETAILS,
message: 'subscription added',
createdAt: '2023-12-11T04:20:00',
branch: 'main',
},
}; };
}); });

View File

@ -1,15 +1,11 @@
import { Environment, Project } from 'gql-client'; import { Project, Deployment } from 'gql-client';
export interface ProjectDetails extends Project { export interface ProjectDetails extends Project {
// TODO: isDomain flag // TODO: isDomain flag
domain?: string | null; domain?: string | null;
// TODO: Use deployment branch // TODO: Use deployment branch
source?: string; source?: string;
latestCommit: { latestCommit: Commit;
message: string;
createdAt: string;
branch: string;
};
// TODO: Move out of project // TODO: Move out of project
repositories?: RepositoryDetails[]; repositories?: RepositoryDetails[];
@ -22,22 +18,9 @@ export interface ProjectMember {
permissions: string[]; permissions: string[];
} }
export interface DeploymentDetails { export interface DeploymentDetails extends Deployment {
id: string; commit: Commit;
title: string;
isProduction: boolean;
domain: DomainDetails;
status: Status;
branch: string;
environment: Environment;
isCurrent: boolean;
commit: {
hash: string;
message: string;
};
author: string; author: string;
createdAt: string;
updatedAt: string;
} }
export enum Status { export enum Status {
@ -92,3 +75,9 @@ export interface Member {
export interface ProjectSearchOutletContext { export interface ProjectSearchOutletContext {
projects: ProjectDetails[]; projects: ProjectDetails[];
} }
export interface Commit {
message: string;
createdAt: string;
branch: string;
}

View File

@ -42,7 +42,7 @@ mutation ($projectId: String!) {
export const rollbackDeployment = gql` export const rollbackDeployment = gql`
mutation ($projectId: String! ,$deploymentId: String!) { mutation ($projectId: String! ,$deploymentId: String!) {
rollbackDeployment(proejctId: $projectId, deploymentId: $deploymentId) rollbackDeployment(projectId: $projectId, deploymentId: $deploymentId)
} }
`; `;