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:
parent
aa49bb0de2
commit
7e2a313012
14
README.md
14
README.md
@ -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
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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 {
|
||||||
|
@ -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 =
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user