diff --git a/packages/backend/src/database.ts b/packages/backend/src/database.ts index 1a71758..6e31ff4 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 08f42c5..eb0ea9a 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 4841a32..bc7b1a7 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 afa676e..09387f3 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 4a0732b..7b76319 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 726390b..d5c1fd8 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 60ba21e..869587f 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 818ddb6..7727f5f 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 5870482..5f8c1e0 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 5f35fba..ec44be4 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 dd2dfe4..ee2ee47 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 b7692af..4384104 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; +}