From 2fb048e8abf648a29f08a9a704cd3eb308dc6f88 Mon Sep 17 00:00:00 2001 From: Nabarun Gogoi Date: Thu, 25 Jan 2024 11:17:44 +0530 Subject: [PATCH] Add GQL mutation to update deployment to production (#39) * Add mutation to update deployment to production * Implement gql client mutation and frontend to update deployment to production * Add toast message when deployment is changed to production * Throw error from init db script if db aleardy exists --------- Co-authored-by: neeraj --- packages/backend/src/database.ts | 16 ++++-- packages/backend/src/entity/Deployment.ts | 2 +- packages/backend/src/resolvers.ts | 14 +++++- packages/backend/src/schema.gql | 1 + packages/backend/test/initialize-db.ts | 2 + .../projects/project/DeploymentsTabPanel.tsx | 49 ++++++++++++++++--- .../projects/project/ProjectTabs.tsx | 2 +- .../deployments/DeploymentDetailsCard.tsx | 21 +++++++- packages/frontend/src/types/project.ts | 1 + packages/gql-client/src/client.ts | 15 +++++- packages/gql-client/src/mutations.ts | 6 +++ packages/gql-client/src/types.ts | 4 ++ 12 files changed, 117 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index 1a71758d..6e31ff41 100644 --- a/packages/backend/src/database.ts +++ b/packages/backend/src/database.ts @@ -1,4 +1,4 @@ -import { DataSource } from 'typeorm'; +import { DataSource, DeepPartial } from 'typeorm'; import path from 'path'; import debug from 'debug'; import assert from 'assert'; @@ -131,8 +131,7 @@ export class Database { return environmentVariables; } - async removeProjectMemberByMemberId (memberId: string): Promise { - // TODO: Check if user is authorized to delete members + async removeProjectMemberById (memberId: string): Promise { const projectMemberRepository = this.dataSource.getRepository(ProjectMember); const deleted = await projectMemberRepository.delete(memberId); @@ -206,4 +205,15 @@ export class Database { return projects; } + + async updateDeploymentById (deploymentId: string, updates: DeepPartial): Promise { + const deploymentRepository = this.dataSource.getRepository(Deployment); + const updatedDeployment = await deploymentRepository.update({ id: Number(deploymentId) }, updates); + + if (updatedDeployment.affected) { + return updatedDeployment.affected > 0; + } else { + return false; + } + } } diff --git a/packages/backend/src/entity/Deployment.ts b/packages/backend/src/entity/Deployment.ts index 08f42c54..eb0ea9a1 100644 --- a/packages/backend/src/entity/Deployment.ts +++ b/packages/backend/src/entity/Deployment.ts @@ -12,7 +12,7 @@ import { import { Project } from './Project'; import { Domain } from './Domain'; -enum Environment { +export enum Environment { Production = 'Production', Preview = 'Preview', Development = 'Development', diff --git a/packages/backend/src/resolvers.ts b/packages/backend/src/resolvers.ts index 4841a320..bc7b1a72 100644 --- a/packages/backend/src/resolvers.ts +++ b/packages/backend/src/resolvers.ts @@ -3,6 +3,7 @@ import assert from 'assert'; import { Database } from './database'; import { deploymentToGqlType, projectMemberToGqlType, projectToGqlType, environmentVariableToGqlType, isUserOwner } from './utils'; +import { Environment } from './entity/Deployment'; const log = debug('snowball:database'); @@ -102,7 +103,7 @@ export const createResolvers = async (db: Database): Promise => { assert(memberProject); if (isUserOwner(String(context.userId), String(memberProject.owner.id))) { - return db.removeProjectMemberByMemberId(memberId); + return db.removeProjectMemberById(memberId); } else { throw new Error('Invalid operation: not authorized'); } @@ -119,6 +120,17 @@ export const createResolvers = async (db: Database): Promise => { log(err); return false; } + }, + + updateDeploymentToProd: async (_: any, { deploymentId }: {deploymentId: string }) => { + try { + return db.updateDeploymentById(deploymentId, { + environment: Environment.Production + }); + } catch (err) { + log(err); + return false; + } } } }; diff --git a/packages/backend/src/schema.gql b/packages/backend/src/schema.gql index afa676e4..09387f3f 100644 --- a/packages/backend/src/schema.gql +++ b/packages/backend/src/schema.gql @@ -124,6 +124,7 @@ type Query { type Mutation { removeMember(memberId: String!): Boolean! addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean! + updateDeploymentToProd(deploymentId: String!): Boolean! } input AddEnvironmentVariableInput { diff --git a/packages/backend/test/initialize-db.ts b/packages/backend/test/initialize-db.ts index 4a0732bb..7b763199 100644 --- a/packages/backend/test/initialize-db.ts +++ b/packages/backend/test/initialize-db.ts @@ -117,6 +117,8 @@ const main = async () => { await dataSource.initialize(); await generateTestData(dataSource); + } else { + throw new Error('Database already exists'); } }; diff --git a/packages/frontend/src/components/projects/project/DeploymentsTabPanel.tsx b/packages/frontend/src/components/projects/project/DeploymentsTabPanel.tsx index 726390b5..d5c1fd87 100644 --- a/packages/frontend/src/components/projects/project/DeploymentsTabPanel.tsx +++ b/packages/frontend/src/components/projects/project/DeploymentsTabPanel.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Button, Typography } from '@material-tailwind/react'; @@ -8,18 +8,48 @@ import FilterForm, { StatusOptions, } from './deployments/FilterForm'; import { DeploymentDetails } from '../../../types/project'; +import { useGQLClient } from '../../../context/GQLClientContext'; const DEFAULT_FILTER_VALUE: FilterValue = { searchedBranch: '', status: StatusOptions.ALL_STATUS, }; -const DeploymentsTabPanel = ({ - deployments, -}: { - deployments: DeploymentDetails[]; -}) => { +const DeploymentsTabPanel = ({ projectId }: { projectId: string }) => { + const client = useGQLClient(); + const [filterValue, setFilterValue] = useState(DEFAULT_FILTER_VALUE); + const [deployments, setDeployments] = useState([]); + + const fetchDeployments = async () => { + const { deployments } = await client.getDeployments(projectId); + const updatedDeployments = deployments.map((deployment: any) => { + return { + ...deployment, + isProduction: deployment.environment === 'Production', + author: '', + commit: { + hash: '', + message: '', + }, + domain: deployment.domain + ? { + ...deployment.domain, + record: { + type: '', + name: '', + value: '', + }, + } + : null, + }; + }); + setDeployments(updatedDeployments); + }; + + useEffect(() => { + fetchDeployments(); + }, []); const productionDeployment = useMemo(() => { return deployments.find((deployment) => { @@ -49,12 +79,16 @@ const DeploymentsTabPanel = ({ return branchMatch && statusMatch && dateMatch; }) as DeploymentDetails[]; - }, [filterValue]); + }, [filterValue, deployments]); const handleResetFilters = useCallback(() => { setFilterValue(DEFAULT_FILTER_VALUE); }, []); + const onUpdateDeploymenToProd = async () => { + await fetchDeployments(); + }; + return (
); }) diff --git a/packages/frontend/src/components/projects/project/ProjectTabs.tsx b/packages/frontend/src/components/projects/project/ProjectTabs.tsx index 60ba21e2..869587fd 100644 --- a/packages/frontend/src/components/projects/project/ProjectTabs.tsx +++ b/packages/frontend/src/components/projects/project/ProjectTabs.tsx @@ -46,7 +46,7 @@ const ProjectTabs = ({ project }: ProjectTabsProps) => { - + diff --git a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx index 818ddb62..7727f5f1 100644 --- a/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx +++ b/packages/frontend/src/components/projects/project/deployments/DeploymentDetailsCard.tsx @@ -9,15 +9,18 @@ import { Chip, ChipProps, } from '@material-tailwind/react'; +import toast from 'react-hot-toast'; import { relativeTimeMs } from '../../../../utils/time'; import ConfirmDialog from '../../../shared/ConfirmDialog'; import DeploymentDialogBodyCard from './DeploymentDialogBodyCard'; import { DeploymentDetails, Status } from '../../../../types/project'; +import { useGQLClient } from '../../../../context/GQLClientContext'; interface DeployDetailsCardProps { deployment: DeploymentDetails; productionDeployment: DeploymentDetails; + onUpdate: () => Promise; } const STATUS_COLORS: { [key in Status]: ChipProps['color'] } = { @@ -29,11 +32,24 @@ const STATUS_COLORS: { [key in Status]: ChipProps['color'] } = { const DeploymentDetailsCard = ({ deployment, productionDeployment, + onUpdate, }: DeployDetailsCardProps) => { + const client = useGQLClient(); + const [changeToProduction, setChangeToProduction] = useState(false); const [redeployToProduction, setRedeployToProduction] = useState(false); const [rollbackDeployment, setRollbackDeployment] = useState(false); + const updateDeployment = async () => { + const isUpdated = await client.updateDeploymentToProd(deployment.id); + if (isUpdated) { + await onUpdate(); + toast.success('Deployment changed to production'); + } else { + toast.error('Unable to change deployment to production'); + } + }; + return (
@@ -97,7 +113,10 @@ const DeploymentDetailsCard = ({ open={changeToProduction} confirmButtonTitle="Change" color="blue" - handleConfirm={() => setChangeToProduction((preVal) => !preVal)} + handleConfirm={() => { + updateDeployment(); + setChangeToProduction((preVal) => !preVal); + }} >
diff --git a/packages/frontend/src/types/project.ts b/packages/frontend/src/types/project.ts index 5870482a..5f8c1e0a 100644 --- a/packages/frontend/src/types/project.ts +++ b/packages/frontend/src/types/project.ts @@ -33,6 +33,7 @@ export interface ProjectMember { } export interface DeploymentDetails { + id: string; title: string; isProduction: boolean; domain: DomainDetails; diff --git a/packages/gql-client/src/client.ts b/packages/gql-client/src/client.ts index 5f35fbad..ec44be49 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 } from './queries'; -import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse } from './types'; -import { removeMember, addEnvironmentVariables } from './mutations'; +import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse } from './types'; +import { removeMember, addEnvironmentVariables, updateDeploymentToProd } from './mutations'; export interface GraphQLConfig { gqlEndpoint: string; @@ -113,4 +113,15 @@ export class GQLClient { return data; } + + async updateDeploymentToProd (deploymentId: string): Promise { + const { data } = await this.client.mutate({ + mutation: updateDeploymentToProd, + variables: { + deploymentId + } + }); + + return data; + } } diff --git a/packages/gql-client/src/mutations.ts b/packages/gql-client/src/mutations.ts index dd2dfe47..ee2ee476 100644 --- a/packages/gql-client/src/mutations.ts +++ b/packages/gql-client/src/mutations.ts @@ -11,3 +11,9 @@ mutation ($projectId: String!, $environmentVariables: [AddEnvironmentVariableInp addEnvironmentVariables(projectId: $projectId, environmentVariables: $environmentVariables) } `; + +export const updateDeploymentToProd = gql` +mutation ($deploymentId: String!) { + updateDeploymentToProd(deploymentId: $deploymentId) +} +`; diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index b7692afa..4384104c 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -165,3 +165,7 @@ export type AddEnvironmentVariableInput = { key: string; value: string; } + +export type UpdateDeploymentToProdResponse = { + updateDeploymentToProd: boolean; +}