Implement functionality to add and delete Deploy webhooks (#47)

* Add mutation to add webhooks

* Implement frontend to add webhooks and refactor code to use updateProject resolver

* Implement functionality to delete webhooks

* Refactor webhook card component

* Update readme for frontend env GitHub OAuth client ID

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-02-01 14:04:07 +05:30 committed by GitHub
parent aa49bb0de2
commit 7e2a313012
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 80 additions and 83 deletions

View File

@ -31,7 +31,7 @@
``` ```
- 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 "Homepage URL", type `http://localhost:3000`
- In "Authorization callback URL", type `http://localhost:3000/projects/create` - In "Authorization callback URL", type `http://localhost:3000/projects/create`
@ -43,16 +43,22 @@
yarn start yarn start
``` ```
- Change directory to `packages/frontend` in a new terminal
```bash
cd packages/frontend
```
- Copy the graphQL endpoint from terminal and add the endpoint in the [.env](packages/frontend/.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'
``` ```
- Change directory to `packages/frontend` - Copy the GitHub OAuth app client ID from previous steps and set it in frontend [.env](packages/frontend/.env) file
```bash ```
cd packages/frontend REACT_APP_GITHUB_CLIENT_ID = <CLIENT_ID>
``` ```
- Start the React application - Start the React application

View File

@ -9,6 +9,7 @@ import { deploymentToGqlType, projectMemberToGqlType, projectToGqlType, environm
import { Environment } from './entity/Deployment'; import { Environment } from './entity/Deployment';
import { Permission } from './entity/ProjectMember'; import { Permission } from './entity/ProjectMember';
import { Domain } from './entity/Domain'; import { Domain } from './entity/Domain';
import { Project } from './entity/Project';
const log = debug('snowball:database'); const log = debug('snowball:database');
@ -188,9 +189,9 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
} }
}, },
updateProject: async (_: any, { projectId, projectDetails }: { projectId: string, projectDetails: { name: string, description: string } }) => { updateProject: async (_: any, { projectId, projectDetails }: { projectId: string, projectDetails: DeepPartial<Project> }) => {
try { try {
return db.updateProjectById(projectId, projectDetails); return await db.updateProjectById(projectId, projectDetails);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@ -273,15 +274,6 @@ 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,7 +139,6 @@ 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!
@ -159,6 +158,8 @@ input AddEnvironmentVariableInput {
input UpdateProjectInput { input UpdateProjectInput {
name: String name: String
description: String description: String
prodBranch: String
webhooks: [String!]
} }
input AddDomainInput { input AddDomainInput {

View File

@ -1,3 +1,3 @@
REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql' REACT_APP_GQL_SERVER_URL = 'http://localhost:8000/graphql'
REACT_APP_GITHUB_CLIENT_ID = 4720362b6740b00652b6 REACT_APP_GITHUB_CLIENT_ID =

View File

@ -17,17 +17,6 @@ const GitTabPanel = ({
}) => { }) => {
const client = useGQLClient(); const client = useGQLClient();
const {
register,
handleSubmit,
reset,
formState: { isSubmitSuccessful },
} = useForm({
defaultValues: {
webhookUrl: project.webhooks,
},
});
const { const {
register: registerProdBranch, register: registerProdBranch,
handleSubmit: handleSubmitProdBranch, handleSubmit: handleSubmitProdBranch,
@ -40,12 +29,11 @@ const GitTabPanel = ({
const updateProdBranchHandler = useCallback( const updateProdBranchHandler = useCallback(
async (data: any) => { async (data: any) => {
const { updateProdBranch } = await client.updateProdBranch( const { updateProject } = await client.updateProject(project.id, {
project.id, prodBranch: data.prodBranch,
data.prodBranch, });
);
if (updateProdBranch) { if (updateProject) {
await onUpdate(); await onUpdate();
toast.success('Production branch upadated successfully'); toast.success('Production branch upadated successfully');
} else { } else {
@ -55,9 +43,33 @@ const GitTabPanel = ({
[project], [project],
); );
useEffect(() => { const {
reset(); register: registerWebhooks,
}, [isSubmitSuccessful]); handleSubmit: handleSubmitWebhooks,
reset: resetWebhooks,
} = useForm({
defaultValues: {
webhookUrl: '',
},
});
const updateWebhooksHandler = useCallback(
async (data: any) => {
const { updateProject } = await client.updateProject(project.id, {
webhooks: [...project.webhooks, data.webhookUrl],
});
if (updateProject) {
await onUpdate();
toast.success('Webhook added successfully');
} else {
toast.error('Error adding webhook');
}
resetWebhooks();
},
[project],
);
useEffect(() => { useEffect(() => {
resetProdBranch({ resetProdBranch({
@ -65,8 +77,18 @@ const GitTabPanel = ({
}); });
}, [project]); }, [project]);
const handleDelete = () => { const handleDeleteWebhook = async (index: number) => {
// TODO: Impletement functionality to delete webhooks project.webhooks.splice(index, 1);
const { updateProject } = await client.updateProject(project.id, {
webhooks: project.webhooks,
});
if (updateProject) {
await onUpdate();
toast.success('Webhook deleted successfully');
} else {
toast.error('Error deleting webhook');
}
}; };
return ( return (
@ -117,11 +139,7 @@ const GitTabPanel = ({
</div> </div>
</form> </form>
<form <form onSubmit={handleSubmitWebhooks(updateWebhooksHandler)}>
onSubmit={handleSubmit(() => {
toast.success('Webhook added successfully.');
})}
>
<div className="mb-2 p-2"> <div className="mb-2 p-2">
<Typography variant="h6" className="text-black"> <Typography variant="h6" className="text-black">
Deploy webhooks Deploy webhooks
@ -133,7 +151,10 @@ const GitTabPanel = ({
<div className="flex gap-1"> <div className="flex gap-1">
<div className="grow"> <div className="grow">
<Typography variant="small">Webhook URL</Typography> <Typography variant="small">Webhook URL</Typography>
<Input crossOrigin={undefined} {...register('webhookUrl')} /> <Input
crossOrigin={undefined}
{...registerWebhooks('webhookUrl')}
/>
</div> </div>
<div className="self-end"> <div className="self-end">
<Button size="sm" type="submit"> <Button size="sm" type="submit">
@ -147,9 +168,8 @@ const GitTabPanel = ({
{project.webhooks.map((webhookUrl, index) => { {project.webhooks.map((webhookUrl, index) => {
return ( return (
<WebhookCard <WebhookCard
webhooksArray={project.webhooks}
webhookUrl={webhookUrl} webhookUrl={webhookUrl}
handleDelete={() => handleDelete()} onDelete={() => handleDeleteWebhook(index)}
key={index} key={index}
/> />
); );

View File

@ -5,21 +5,22 @@ import { Button, Typography } from '@material-tailwind/react';
import ConfirmDialog from '../../../shared/ConfirmDialog'; import ConfirmDialog from '../../../shared/ConfirmDialog';
const WebhookCard = (props: { interface WebhookCardProps {
webhooksArray: string[];
webhookUrl: string; webhookUrl: string;
handleDelete: () => void; onDelete: () => void;
}) => { }
const WebhookCard = ({ webhookUrl, onDelete }: WebhookCardProps) => {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
return ( return (
<div className="flex justify-between w-full mb-3"> <div className="flex justify-between w-full mb-3">
{props.webhookUrl} {webhookUrl}
<div className="flex gap-3"> <div className="flex gap-3">
<Button <Button
size="sm" size="sm"
onClick={() => { onClick={() => {
navigator.clipboard.writeText(props.webhookUrl); navigator.clipboard.writeText(webhookUrl);
toast.success('Copied to clipboard'); toast.success('Copied to clipboard');
}} }}
> >
@ -43,16 +44,13 @@ const WebhookCard = (props: {
confirmButtonTitle="Yes, Confirm delete" confirmButtonTitle="Yes, Confirm delete"
handleConfirm={() => { handleConfirm={() => {
setDeleteDialogOpen((preVal) => !preVal); setDeleteDialogOpen((preVal) => !preVal);
props.handleDelete(); onDelete();
}} }}
color="red" color="red"
> >
<Typography variant="small"> <Typography variant="small">
Are you sure you want to delete the variable{' '} Are you sure you want to delete the variable{' '}
<span className="bg-blue-100 p-0.5 rounded-sm"> <span className="bg-blue-100 p-0.5 rounded-sm">{webhookUrl}</span>?
{props.webhookUrl}
</span>
?
</Typography> </Typography>
</ConfirmDialog> </ConfirmDialog>
</div> </div>

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, UpdateProdBranchResponse } 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 } from './types';
import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGitHub, unauthenticateGitHub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember, deleteDomain, updateProdBranch } from './mutations'; import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGitHub, unauthenticateGitHub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember, deleteDomain } from './mutations';
export interface GraphQLConfig { export interface GraphQLConfig {
gqlEndpoint: string; gqlEndpoint: string;
@ -292,16 +292,4 @@ 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,9 +86,3 @@ export const unauthenticateGitHub = gql`
mutation { mutation {
unauthenticateGitHub unauthenticateGitHub
}`; }`;
export const updateProdBranch = gql`
mutation ($projectId: String!, $prodBranch: String!) {
updateProdBranch(projectId: $projectId, prodBranch: $prodBranch)
}
`;

View File

@ -223,8 +223,10 @@ export type DeleteDomainResponse = {
} }
export type UpdateProjectInput = { export type UpdateProjectInput = {
name: string name?: string
description: string description?: string
prodBranch?: string
webhooks?: string[]
} }
export type UpdateDomainInput = { export type UpdateDomainInput = {
@ -258,7 +260,3 @@ export type AuthenticateGitHubResponse = {
export type UnauthenticateGitHubResponse = { export type UnauthenticateGitHubResponse = {
unauthenticateGitHub: boolean unauthenticateGitHub: boolean
} }
export type UpdateProdBranchResponse = {
updateProdBranch: boolean
}