Show public repo GitHub activity without authentication (#68)

* Handle request error while fetching Git activity

* Use default Octokit instance if auth token is not available

* Rename input arguments of mutation methods to data

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-02-14 17:35:02 +05:30 committed by GitHub
parent db3b9148b6
commit 9921dc5186
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 103 additions and 90 deletions

View File

@ -236,9 +236,9 @@ export class Database {
return savedEnvironmentVariables; return savedEnvironmentVariables;
} }
async updateEnvironmentVariable (environmentVariableId: string, update: DeepPartial<EnvironmentVariable>): Promise<boolean> { async updateEnvironmentVariable (environmentVariableId: string, data: DeepPartial<EnvironmentVariable>): Promise<boolean> {
const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable); const environmentVariableRepository = this.dataSource.getRepository(EnvironmentVariable);
const updateResult = await environmentVariableRepository.update({ id: environmentVariableId }, update); const updateResult = await environmentVariableRepository.update({ id: environmentVariableId }, data);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
@ -293,18 +293,18 @@ export class Database {
return projects; return projects;
} }
async updateDeploymentById (deploymentId: string, updates: DeepPartial<Deployment>): Promise<boolean> { async updateDeploymentById (deploymentId: string, data: DeepPartial<Deployment>): Promise<boolean> {
const deploymentRepository = this.dataSource.getRepository(Deployment); const deploymentRepository = this.dataSource.getRepository(Deployment);
const updateResult = await deploymentRepository.update({ id: deploymentId }, updates); const updateResult = await deploymentRepository.update({ id: deploymentId }, data);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async addProject (userId: string, organizationId: string, projectDetails: DeepPartial<Project>): Promise<Project> { async addProject (userId: string, organizationId: string, data: DeepPartial<Project>): Promise<Project> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
// TODO: Check if organization exists // TODO: Check if organization exists
const newProject = projectRepository.create(projectDetails); const newProject = projectRepository.create(data);
// TODO: Set default empty array for webhooks in TypeORM // TODO: Set default empty array for webhooks in TypeORM
newProject.webhooks = []; newProject.webhooks = [];
// TODO: Set icon according to framework // TODO: Set icon according to framework
@ -323,9 +323,9 @@ export class Database {
return projectRepository.save(newProject); return projectRepository.save(newProject);
} }
async updateProjectById (projectId: string, updates: DeepPartial<Project>): Promise<boolean> { async updateProjectById (projectId: string, data: DeepPartial<Project>): Promise<boolean> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const updateResult = await projectRepository.update({ id: projectId }, updates); const updateResult = await projectRepository.update({ id: projectId }, data);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
@ -372,9 +372,9 @@ export class Database {
return domain; return domain;
} }
async updateDomainById (domainId: string, updates: DeepPartial<Domain>): Promise<boolean> { async updateDomainById (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
const domainRepository = this.dataSource.getRepository(Domain); const domainRepository = this.dataSource.getRepository(Domain);
const updateResult = await domainRepository.update({ id: domainId }, updates); const updateResult = await domainRepository.update({ id: domainId }, data);
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }

View File

@ -135,9 +135,9 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
updateProject: async (_: any, { projectId, projectDetails }: { projectId: string, projectDetails: DeepPartial<Project> }) => { updateProject: async (_: any, { projectId, data }: { projectId: string, data: DeepPartial<Project> }) => {
try { try {
return await service.updateProject(projectId, projectDetails); return await service.updateProject(projectId, data);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
@ -179,18 +179,18 @@ export const createResolvers = async (service: Service): Promise<any> => {
} }
}, },
addDomain: async (_: any, { projectId, domainDetails }: { projectId: string, domainDetails: { name: string } }) => { addDomain: async (_: any, { projectId, data }: { projectId: string, data: { name: string } }) => {
try { try {
return Boolean(await service.addDomain(projectId, domainDetails)); return Boolean(await service.addDomain(projectId, data));
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;
} }
}, },
updateDomain: async (_: any, { domainId, domainDetails }: { domainId: string, domainDetails: DeepPartial<Domain>}) => { updateDomain: async (_: any, { domainId, data }: { domainId: string, data: DeepPartial<Domain>}) => {
try { try {
return await service.updateDomain(domainId, domainDetails); return await service.updateDomain(domainId, data);
} catch (err) { } catch (err) {
log(err); log(err);
return false; return false;

View File

@ -195,13 +195,13 @@ type Mutation {
removeEnvironmentVariable(environmentVariableId: String!): Boolean! removeEnvironmentVariable(environmentVariableId: String!): Boolean!
updateDeploymentToProd(deploymentId: String!): Boolean! updateDeploymentToProd(deploymentId: String!): Boolean!
addProject(organizationSlug: String!, data: AddProjectInput): Project! addProject(organizationSlug: String!, data: AddProjectInput): Project!
updateProject(projectId: String!, projectDetails: UpdateProjectInput): Boolean! updateProject(projectId: String!, data: UpdateProjectInput): 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!
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean! rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
addDomain(projectId: String!, domainDetails: AddDomainInput!): Boolean! addDomain(projectId: String!, data: AddDomainInput!): Boolean!
updateDomain(domainId: String!, domainDetails: UpdateDomainInput!): Boolean! updateDomain(domainId: String!, data: UpdateDomainInput!): Boolean!
authenticateGitHub(code: String!): AuthResult! authenticateGitHub(code: String!): AuthResult!
unauthenticateGitHub: Boolean! unauthenticateGitHub: Boolean!
} }

View File

@ -405,7 +405,7 @@ export class Service {
return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate; return newCurrentDeploymentUpdate && oldCurrentDeploymentUpdate;
} }
async addDomain (projectId: string, domainDetails: { name: string }): Promise<{ async addDomain (projectId: string, data: { name: string }): Promise<{
primaryDomain: Domain, primaryDomain: Domain,
redirectedDomain: Domain redirectedDomain: Domain
}> { }> {
@ -416,14 +416,14 @@ export class Service {
} }
const primaryDomainDetails = { const primaryDomainDetails = {
...domainDetails, ...data,
branch: currentProject.prodBranch, branch: currentProject.prodBranch,
project: currentProject project: currentProject
}; };
const savedPrimaryDomain = await this.db.addDomain(primaryDomainDetails); const savedPrimaryDomain = await this.db.addDomain(primaryDomainDetails);
const domainArr = domainDetails.name.split('www.'); const domainArr = data.name.split('www.');
const redirectedDomainDetails = { const redirectedDomainDetails = {
name: domainArr.length > 1 ? domainArr[1] : `www.${domainArr[0]}`, name: domainArr.length > 1 ? domainArr[1] : `www.${domainArr[0]}`,
@ -437,7 +437,7 @@ export class Service {
return { primaryDomain: savedPrimaryDomain, redirectedDomain: savedRedirectedDomain }; return { primaryDomain: savedPrimaryDomain, redirectedDomain: savedRedirectedDomain };
} }
async updateDomain (domainId: string, domainDetails: DeepPartial<Domain>): Promise<boolean> { async updateDomain (domainId: string, data: DeepPartial<Domain>): Promise<boolean> {
const domain = await this.db.getDomain({ const domain = await this.db.getDomain({
where: { where: {
id: domainId id: domainId
@ -449,7 +449,7 @@ export class Service {
} }
const newDomain = { const newDomain = {
...domainDetails ...data
}; };
const domainsRedirectedFrom = await this.db.getDomains({ const domainsRedirectedFrom = await this.db.getDomains({
@ -462,14 +462,14 @@ export class Service {
}); });
// If there are domains redirecting to current domain, only branch of current domain can be updated // If there are domains redirecting to current domain, only branch of current domain can be updated
if (domainsRedirectedFrom.length > 0 && domainDetails.branch === domain.branch) { if (domainsRedirectedFrom.length > 0 && data.branch === domain.branch) {
throw new Error('Remove all redirects to this domain before updating'); throw new Error('Remove all redirects to this domain before updating');
} }
if (domainDetails.redirectToId) { if (data.redirectToId) {
const redirectedDomain = await this.db.getDomain({ const redirectedDomain = await this.db.getDomain({
where: { where: {
id: domainDetails.redirectToId id: data.redirectToId
} }
}); });

View File

@ -1,5 +1,3 @@
export const ORGANIZATION_ID = '2379cf1f-a232-4ad2-ae14-4d881131cc26';
export const GIT_TEMPLATE_LINK = export const GIT_TEMPLATE_LINK =
'https://git.vdb.to/cerc-io/test-progressive-web-app'; 'https://git.vdb.to/cerc-io/test-progressive-web-app';

View File

@ -15,16 +15,20 @@ const UNAUTHORIZED_ERROR_CODE = 401;
interface ContextValue { interface ContextValue {
octokit: Octokit | null; octokit: Octokit | null;
isAuth: boolean;
updateAuth: () => void; updateAuth: () => void;
} }
const OctokitContext = createContext<ContextValue>({ const OctokitContext = createContext<ContextValue>({
octokit: null, octokit: null,
isAuth: false,
updateAuth: () => {}, updateAuth: () => {},
}); });
export const OctokitProvider = ({ children }: { children: ReactNode }) => { export const OctokitProvider = ({ children }: { children: ReactNode }) => {
const [authToken, setAuthToken] = useState<string>(''); const [authToken, setAuthToken] = useState<string>('');
const [isAuth, setIsAuth] = useState(false);
const client = useGQLClient(); const client = useGQLClient();
const fetchUser = useCallback(async () => { const fetchUser = useCallback(async () => {
@ -41,9 +45,11 @@ export const OctokitProvider = ({ children }: { children: ReactNode }) => {
const octokit = useMemo(() => { const octokit = useMemo(() => {
if (!authToken) { if (!authToken) {
return null; setIsAuth(false);
return new Octokit();
} }
setIsAuth(true);
return new Octokit({ auth: authToken }); return new Octokit({ auth: authToken });
}, [authToken]); }, [authToken]);
@ -52,10 +58,6 @@ export const OctokitProvider = ({ children }: { children: ReactNode }) => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!octokit) {
return;
}
// TODO: Handle React component error // TODO: Handle React component error
const interceptor = async (error: RequestError | Error) => { const interceptor = async (error: RequestError | Error) => {
if ( if (
@ -78,14 +80,14 @@ export const OctokitProvider = ({ children }: { children: ReactNode }) => {
}, [octokit, client]); }, [octokit, client]);
return ( return (
<OctokitContext.Provider value={{ octokit, updateAuth }}> <OctokitContext.Provider value={{ octokit, updateAuth, isAuth }}>
{children} {children}
</OctokitContext.Provider> </OctokitContext.Provider>
); );
}; };
export const useOctokit = () => { export const useOctokit = () => {
const { octokit, updateAuth } = useContext(OctokitContext); const { octokit, updateAuth, isAuth } = useContext(OctokitContext);
return { octokit, updateAuth }; return { octokit, updateAuth, isAuth };
}; };

View File

@ -7,9 +7,9 @@ import ConnectAccount from '../../../../components/projects/create/ConnectAccoun
import { useOctokit } from '../../../../context/OctokitContext'; import { useOctokit } from '../../../../context/OctokitContext';
const NewProject = () => { const NewProject = () => {
const { octokit, updateAuth } = useOctokit(); const { octokit, updateAuth, isAuth } = useOctokit();
return Boolean(octokit) ? ( return isAuth ? (
<> <>
<h5 className="mt-4 ml-4">Start with template</h5> <h5 className="mt-4 ml-4">Start with template</h5>
<div className="grid grid-cols-3 p-4 gap-4"> <div className="grid grid-cols-3 p-4 gap-4">

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Domain, DomainStatus } from 'gql-client'; import { Domain, DomainStatus } from 'gql-client';
import { useOutletContext } from 'react-router-dom'; import { useNavigate, useOutletContext } from 'react-router-dom';
import { RequestError } from 'octokit';
import { Typography, Button, Chip, Avatar } from '@material-tailwind/react'; import { Typography, Button, Chip, Avatar } from '@material-tailwind/react';
@ -14,6 +15,7 @@ const COMMITS_PER_PAGE = 4;
const OverviewTabPanel = () => { const OverviewTabPanel = () => {
const { octokit } = useOctokit(); const { octokit } = useOctokit();
const navigate = useNavigate();
const [activities, setActivities] = useState<GitCommitDetails[]>([]); const [activities, setActivities] = useState<GitCommitDetails[]>([]);
const [liveDomain, setLiveDomain] = useState<Domain>(); const [liveDomain, setLiveDomain] = useState<Domain>();
@ -29,47 +31,55 @@ const OverviewTabPanel = () => {
// TODO: Save repo commits in DB and avoid using GitHub API in frontend // TODO: Save repo commits in DB and avoid using GitHub API in frontend
// TODO: Figure out fetching latest commits for all branches // TODO: Figure out fetching latest commits for all branches
const fetchRepoActivity = async () => { const fetchRepoActivity = async () => {
const [owner, repo] = project.repository.split('/'); try {
const [owner, repo] = project.repository.split('/');
if (!repo) { if (!repo) {
// Do not fetch branches if repo not available // Do not fetch branches if repo not available
return; return;
} }
// Get all branches in project repo // Get all branches in project repo
const result = await octokit.rest.repos.listBranches({ const result = await octokit.rest.repos.listBranches({
owner,
repo,
});
// Get first 4 commits from repo branches
const commitsByBranchPromises = result.data.map(async (branch) => {
const result = await octokit.rest.repos.listCommits({
owner, owner,
repo, repo,
sha: branch.commit.sha,
per_page: COMMITS_PER_PAGE,
}); });
return result.data.map((data) => ({ // Get first 4 commits from repo branches
...data, const commitsByBranchPromises = result.data.map(async (branch) => {
branch, const result = await octokit.rest.repos.listCommits({
})); owner,
}); repo,
sha: branch.commit.sha,
per_page: COMMITS_PER_PAGE,
});
const commitsByBranch = await Promise.all(commitsByBranchPromises); return result.data.map((data) => ({
const commitsWithBranch = commitsByBranch.flat(); ...data,
branch,
}));
});
// Order commits by date and set latest 4 commits in activity section const commitsByBranch = await Promise.all(commitsByBranchPromises);
const orderedCommits = commitsWithBranch const commitsWithBranch = commitsByBranch.flat();
.sort(
(a, b) =>
new Date(b.commit.author!.date!).getTime() -
new Date(a.commit.author!.date!).getTime(),
)
.slice(0, COMMITS_PER_PAGE);
setActivities(orderedCommits); // Order commits by date and set latest 4 commits in activity section
const orderedCommits = commitsWithBranch
.sort(
(a, b) =>
new Date(b.commit.author!.date!).getTime() -
new Date(a.commit.author!.date!).getTime(),
)
.slice(0, COMMITS_PER_PAGE);
setActivities(orderedCommits);
} catch (err) {
if (!(err instanceof RequestError)) {
throw err;
}
console.log(err.message);
}
}; };
fetchRepoActivity(); fetchRepoActivity();
@ -127,6 +137,9 @@ const OverviewTabPanel = () => {
className="normal-case rounded-full" className="normal-case rounded-full"
color="blue" color="blue"
size="sm" size="sm"
onClick={() => {
navigate('settings/domains');
}}
> >
Setup Setup
</Button> </Button>

View File

@ -206,28 +206,28 @@ export class GQLClient {
return result.data; return result.data;
} }
async updateProject (projectId: string, projectDetails: types.UpdateProjectInput): Promise<types.UpdateProjectResponse> { async updateProject (projectId: string, data: types.UpdateProjectInput): Promise<types.UpdateProjectResponse> {
const { data } = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.updateProjectMutation, mutation: mutations.updateProjectMutation,
variables: { variables: {
projectId, projectId,
projectDetails data
} }
}); });
return data; return result.data;
} }
async updateDomain (domainId: string, domainDetails: types.UpdateDomainInput): Promise<types.UpdateDomainResponse> { async updateDomain (domainId: string, data: types.UpdateDomainInput): Promise<types.UpdateDomainResponse> {
const { data } = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.updateDomainMutation, mutation: mutations.updateDomainMutation,
variables: { variables: {
domainId, domainId,
domainDetails data
} }
}); });
return data; return result.data;
} }
async redeployToProd (deploymentId: string): Promise<types.RedeployToProdResponse> { async redeployToProd (deploymentId: string): Promise<types.RedeployToProdResponse> {
@ -275,16 +275,16 @@ export class GQLClient {
return data; return data;
} }
async addDomain (projectId: string, domainDetails: types.AddDomainInput): Promise<types.AddDomainResponse> { async addDomain (projectId: string, data: types.AddDomainInput): Promise<types.AddDomainResponse> {
const { data } = await this.client.mutate({ const result = await this.client.mutate({
mutation: mutations.addDomain, mutation: mutations.addDomain,
variables: { variables: {
projectId, projectId,
domainDetails data
} }
}); });
return data; return result.data;
} }
async getDomains (projectId: string, filter?: types.FilterDomainInput): Promise<types.GetDomainsResponse> { async getDomains (projectId: string, filter?: types.FilterDomainInput): Promise<types.GetDomainsResponse> {

View File

@ -50,13 +50,13 @@ mutation ($organizationSlug: String!, $data: AddProjectInput) {
}`; }`;
export const updateProjectMutation = gql` export const updateProjectMutation = gql`
mutation ($projectId: String!, $projectDetails: UpdateProjectInput) { mutation ($projectId: String!, $data: UpdateProjectInput) {
updateProject(projectId: $projectId, projectDetails: $projectDetails) updateProject(projectId: $projectId, data: $data)
}`; }`;
export const updateDomainMutation = gql` export const updateDomainMutation = gql`
mutation ($domainId: String!, $domainDetails: UpdateDomainInput!) { mutation ($domainId: String!, $data: UpdateDomainInput!) {
updateDomain(domainId: $domainId, domainDetails: $domainDetails) updateDomain(domainId: $domainId, data: $data)
}`; }`;
export const redeployToProd = gql` export const redeployToProd = gql`
@ -83,8 +83,8 @@ mutation ($projectId: String! ,$deploymentId: String!) {
`; `;
export const addDomain = gql` export const addDomain = gql`
mutation ($projectId: String!, $domainDetails: AddDomainInput!) { mutation ($projectId: String!, $data: AddDomainInput!) {
addDomain(projectId: $projectId, domainDetails: $domainDetails) addDomain(projectId: $projectId, data: $data)
} }
`; `;