Implement functionality to update production branch in project settings (#60)

* Add mutation to update prod branch for project

* Implement frontend to update production branch

* Handle review changes

* Update README for Github creating OAuth app

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-02-01 11:18:54 +05:30 committed by Ashwin Phatak
parent 1d58beb2ec
commit aa49bb0de2
8 changed files with 116 additions and 54 deletions

View File

@ -33,6 +33,9 @@
- Set `githubOauth.clientId` and `githubOauth.clientSecret` in backend [config file](packages/backend/environments/local.toml) - Set `githubOauth.clientId` and `githubOauth.clientSecret` in backend [config file](packages/backend/environments/local.toml)
- Client id and secret will be available after creating Github OAuth app - Client id and secret will be available after creating Github OAuth app
- https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app - https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app
- In "Homepage URL", type `http://localhost:3000`
- In "Authorization callback URL", type `http://localhost:3000/projects/create`
- Generate a new client secret after app is created
- Start the server - Start the server
@ -40,7 +43,7 @@
yarn start yarn start
``` ```
- Copy the graphQL endpoint from terminal and add the endpoint in the `.env` file present in `packages/frontend` - Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.env) file present in `packages/frontend`
``` ```
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql' REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'

View File

@ -273,6 +273,15 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
log(err); log(err);
return false; return false;
} }
},
updateProdBranch: async (_: any, { projectId, prodBranch }: {projectId: string, prodBranch: string }) => {
try {
return await db.updateProjectById(projectId, { prodBranch });
} catch (err) {
log(err);
return (false);
}
} }
} }
}; };

View File

@ -139,6 +139,7 @@ type Mutation {
updateEnvironmentVariable(environmentVariableId: String!, environmentVariable: UpdateEnvironmentVariableInput!): Boolean! updateEnvironmentVariable(environmentVariableId: String!, environmentVariable: UpdateEnvironmentVariableInput!): Boolean!
updateDeploymentToProd(deploymentId: String!): Boolean! updateDeploymentToProd(deploymentId: String!): Boolean!
updateProject(projectId: String!, projectDetails: UpdateProjectInput): Boolean! updateProject(projectId: String!, projectDetails: UpdateProjectInput): Boolean!
updateProdBranch(projectId: String!, prodBranch: String!): Boolean!
redeployToProd(deploymentId: String!): Boolean! redeployToProd(deploymentId: String!): Boolean!
deleteProject(projectId: String!): Boolean! deleteProject(projectId: String!): Boolean!
deleteDomain(domainId: String!): Boolean! deleteDomain(domainId: String!): Boolean!

View File

@ -1,32 +1,72 @@
import React, { useEffect, useState } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { Project } from 'gql-client';
import { Button, Input, Switch, Typography } from '@material-tailwind/react'; import { Button, Input, Switch, Typography } from '@material-tailwind/react';
import { GitRepositoryDetails } from '../../../../types/project';
import WebhookCard from './WebhookCard'; import WebhookCard from './WebhookCard';
import { useGQLClient } from '../../../../context/GQLClientContext';
const GitTabPanel = () => { const GitTabPanel = ({
// TODO: Get linked repo from project project,
const [linkedRepo] = useState<GitRepositoryDetails>(); onUpdate,
const [webhooksArray, setWebhooksArray] = useState<Array<string>>([]); }: {
project: Project;
onUpdate: () => Promise<void>;
}) => {
const client = useGQLClient();
const { const {
register, register,
handleSubmit, handleSubmit,
reset, reset,
formState: { isSubmitSuccessful }, formState: { isSubmitSuccessful },
} = useForm(); } = useForm({
defaultValues: {
webhookUrl: project.webhooks,
},
});
const {
register: registerProdBranch,
handleSubmit: handleSubmitProdBranch,
reset: resetProdBranch,
} = useForm({
defaultValues: {
prodBranch: project.prodBranch,
},
});
const updateProdBranchHandler = useCallback(
async (data: any) => {
const { updateProdBranch } = await client.updateProdBranch(
project.id,
data.prodBranch,
);
if (updateProdBranch) {
await onUpdate();
toast.success('Production branch upadated successfully');
} else {
toast.error('Error updating production branch');
}
},
[project],
);
useEffect(() => { useEffect(() => {
reset(); reset();
}, [isSubmitSuccessful]); }, [isSubmitSuccessful]);
const handleDelete = (index: number) => { useEffect(() => {
const newArray = webhooksArray.filter((value, idx) => idx != index); resetProdBranch({
setWebhooksArray(newArray); prodBranch: project.prodBranch,
toast.success('Webhook deleted successfully'); });
}, [project]);
const handleDelete = () => {
// TODO: Impletement functionality to delete webhooks
}; };
return ( return (
@ -55,44 +95,30 @@ const GitTabPanel = () => {
</div> </div>
</div> </div>
<form onSubmit={handleSubmitProdBranch(updateProdBranchHandler)}>
<div className="mb-2 p-2"> <div className="mb-2 p-2">
<Typography variant="h6" className="text-black"> <Typography variant="h6" className="text-black">
Production branch Production branch
</Typography> </Typography>
<Typography variant="small"> <Typography variant="small">
By default, each commit pushed to the{' '} By default, each commit pushed to the{' '}
<span className="font-bold">main</span> branch initiates a production <span className="font-bold">{project.prodBranch}</span> branch
deployment. You can opt for a different branch for deployment in the initiates a production deployment. You can opt for a different
settings. branch for deployment in the settings.
</Typography> </Typography>
{!linkedRepo && (
<div className="flex bg-blue-100 gap-4 rounded-lg p-2">
<div>^</div>
<div>
<Typography variant="small">
This project isn&apos;t currently linked to a Git repository. To
establish a production branch, please linked to an existing Git
repository in the &apos;Connected Git Repository&apos; section
above.
</Typography>
</div>
</div>
)}
<Typography variant="small">Branch name</Typography> <Typography variant="small">Branch name</Typography>
<Input <Input
crossOrigin={undefined} crossOrigin={undefined}
disabled={Boolean(!linkedRepo)} {...registerProdBranch('prodBranch')}
value="main"
/> />
<Button size="sm" disabled className="mt-1"> <Button size="sm" className="mt-1" type="submit">
Save Save
</Button> </Button>
</div> </div>
</form>
<form <form
onSubmit={handleSubmit((data) => { onSubmit={handleSubmit(() => {
setWebhooksArray((prevArray) => [...prevArray, data.webhookUrl]);
toast.success('Webhook added successfully.'); toast.success('Webhook added successfully.');
})} })}
> >
@ -118,12 +144,12 @@ const GitTabPanel = () => {
</div> </div>
</form> </form>
<div className="mb-2 p-2"> <div className="mb-2 p-2">
{webhooksArray?.map((webhookUrl, index) => { {project.webhooks.map((webhookUrl, index) => {
return ( return (
<WebhookCard <WebhookCard
webhooksArray={webhooksArray} webhooksArray={project.webhooks}
webhookUrl={webhookUrl} webhookUrl={webhookUrl}
handleDelete={() => handleDelete(index)} handleDelete={() => handleDelete()}
key={index} key={index}
/> />
); );

View File

@ -41,6 +41,7 @@ const NewProject = () => {
return; return;
} }
// TODO: Handle React component error
const interceptor = async (error: RequestError | Error) => { const interceptor = async (error: RequestError | Error) => {
if ( if (
error instanceof RequestError && error instanceof RequestError &&

View File

@ -1,8 +1,8 @@
import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getDomains, getProjectsInOrganization } from './queries'; import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getDomains, getProjectsInOrganization } from './queries';
import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse, AddDomainInput, AddDomainResponse, GetDomainsResponse, UpdateDomainInput, UpdateDomainResponse, AuthenticateGitHubResponse, UnauthenticateGitHubResponse, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse, UpdateProjectMemberInput, RemoveProjectMemberResponse, UpdateProjectMemberResponse, DeleteDomainResponse } from './types'; import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse, RollbackDeploymentResponse, AddDomainInput, AddDomainResponse, GetDomainsResponse, UpdateDomainInput, UpdateDomainResponse, AuthenticateGitHubResponse, UnauthenticateGitHubResponse, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableInput, RemoveEnvironmentVariableResponse, UpdateProjectMemberInput, RemoveProjectMemberResponse, UpdateProjectMemberResponse, DeleteDomainResponse, UpdateProdBranchResponse } from './types';
import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGitHub, unauthenticateGitHub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember, deleteDomain } from './mutations'; import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGitHub, unauthenticateGitHub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember, deleteDomain, updateProdBranch } from './mutations';
export interface GraphQLConfig { export interface GraphQLConfig {
gqlEndpoint: string; gqlEndpoint: string;
@ -292,4 +292,16 @@ export class GQLClient {
return data; return data;
} }
async updateProdBranch (projectId: string, prodBranch: string): Promise<UpdateProdBranchResponse> {
const { data } = await this.client.mutate({
mutation: updateProdBranch,
variables: {
projectId,
prodBranch
}
});
return data;
}
} }

View File

@ -86,3 +86,9 @@ export const unauthenticateGitHub = gql`
mutation { mutation {
unauthenticateGitHub unauthenticateGitHub
}`; }`;
export const updateProdBranch = gql`
mutation ($projectId: String!, $prodBranch: String!) {
updateProdBranch(projectId: $projectId, prodBranch: $prodBranch)
}
`;

View File

@ -258,3 +258,7 @@ export type AuthenticateGitHubResponse = {
export type UnauthenticateGitHubResponse = { export type UnauthenticateGitHubResponse = {
unauthenticateGitHub: boolean unauthenticateGitHub: boolean
} }
export type UpdateProdBranchResponse = {
updateProdBranch: boolean
}