forked from cerc-io/snowballtools-base
Implement creating project by importing repository (#49)
* Implement create project with import repository * Add button for creating project in deploy step
This commit is contained in:
parent
0aa35d05f4
commit
ef89d69577
@ -326,6 +326,27 @@ export class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addProject (userId: string, projectDetails: DeepPartial<Project>): Promise<Project> {
|
||||||
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
|
// TODO: Check if organization exists
|
||||||
|
const newProject = projectRepository.create(projectDetails);
|
||||||
|
// TODO: Set default empty array for webhooks in TypeORM
|
||||||
|
newProject.webhooks = [];
|
||||||
|
// TODO: Set icon according to framework
|
||||||
|
newProject.icon = '';
|
||||||
|
|
||||||
|
newProject.owner = Object.assign(new User(), {
|
||||||
|
id: Number(userId)
|
||||||
|
});
|
||||||
|
|
||||||
|
newProject.organization = Object.assign(new Organization(), {
|
||||||
|
id: Number(projectDetails.organizationId)
|
||||||
|
});
|
||||||
|
|
||||||
|
return projectRepository.save(newProject);
|
||||||
|
}
|
||||||
|
|
||||||
async updateProjectById (projectId: string, updates: DeepPartial<Project>): Promise<boolean> {
|
async updateProjectById (projectId: string, updates: 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 }, updates);
|
||||||
|
@ -28,6 +28,9 @@ export class Project {
|
|||||||
@JoinColumn({ name: 'organizationId' })
|
@JoinColumn({ name: 'organizationId' })
|
||||||
organization!: Organization | null;
|
organization!: Organization | null;
|
||||||
|
|
||||||
|
@Column('integer')
|
||||||
|
organizationId!: number;
|
||||||
|
|
||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
@ -37,14 +40,14 @@ export class Project {
|
|||||||
@Column('varchar', { length: 255, default: 'main' })
|
@Column('varchar', { length: 255, default: 'main' })
|
||||||
prodBranch!: string;
|
prodBranch!: string;
|
||||||
|
|
||||||
@Column('text')
|
@Column('text', { default: '' })
|
||||||
description!: string;
|
description!: string;
|
||||||
|
|
||||||
@Column('varchar')
|
@Column('varchar', { nullable: true })
|
||||||
template!: string;
|
template!: string | null;
|
||||||
|
|
||||||
@Column('varchar')
|
@Column('varchar', { nullable: true })
|
||||||
framework!: string;
|
framework!: string | null;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: 'simple-array'
|
type: 'simple-array'
|
||||||
|
@ -206,6 +206,16 @@ export const createResolvers = async (db: Database, app: OAuthApp): Promise<any>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addProject: async (_: any, { projectDetails }: { projectDetails: DeepPartial<Project> }, context: any) => {
|
||||||
|
try {
|
||||||
|
await db.addProject(context.userId, projectDetails);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
log(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
updateProject: async (_: any, { projectId, projectDetails }: { projectId: string, projectDetails: DeepPartial<Project> }) => {
|
updateProject: async (_: any, { projectId, projectDetails }: { projectId: string, projectDetails: DeepPartial<Project> }) => {
|
||||||
try {
|
try {
|
||||||
return await db.updateProjectById(projectId, projectDetails);
|
return await db.updateProjectById(projectId, projectDetails);
|
||||||
|
@ -64,7 +64,7 @@ type Project {
|
|||||||
prodBranch: String!
|
prodBranch: String!
|
||||||
description: String
|
description: String
|
||||||
template: String
|
template: String
|
||||||
framework: String!
|
framework: String
|
||||||
webhooks: [String!]
|
webhooks: [String!]
|
||||||
members: [ProjectMember!]
|
members: [ProjectMember!]
|
||||||
environmentVariables: [EnvironmentVariable!]
|
environmentVariables: [EnvironmentVariable!]
|
||||||
@ -116,48 +116,23 @@ type EnvironmentVariable {
|
|||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
|
||||||
user: User!
|
|
||||||
organizations: [Organization!]
|
|
||||||
projects: [Project!]
|
|
||||||
projectsInOrganization(organizationId: String!): [Project!]
|
|
||||||
project(projectId: String!): Project
|
|
||||||
deployments(projectId: String!): [Deployment!]
|
|
||||||
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
|
||||||
projectMembers(projectId: String!): [ProjectMember!]
|
|
||||||
searchProjects(searchText: String!): [Project!]
|
|
||||||
domains(projectId: String!): [Domain!]
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthResult {
|
type AuthResult {
|
||||||
token: String!
|
token: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
|
||||||
removeProjectMember(projectMemberId: String!): Boolean!
|
|
||||||
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
|
|
||||||
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
|
||||||
addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean!
|
|
||||||
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
|
||||||
updateEnvironmentVariable(environmentVariableId: String!, environmentVariable: UpdateEnvironmentVariableInput!): Boolean!
|
|
||||||
updateDeploymentToProd(deploymentId: String!): Boolean!
|
|
||||||
updateProject(projectId: String!, projectDetails: UpdateProjectInput): Boolean!
|
|
||||||
redeployToProd(deploymentId: String!): Boolean!
|
|
||||||
deleteProject(projectId: String!): Boolean!
|
|
||||||
deleteDomain(domainId: String!): Boolean!
|
|
||||||
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
|
|
||||||
addDomain(projectId: String!, domainDetails: AddDomainInput!): Boolean!
|
|
||||||
updateDomain(domainId: String!, domainDetails: UpdateDomainInput!): Boolean!
|
|
||||||
authenticateGitHub(code: String!): AuthResult!
|
|
||||||
unauthenticateGitHub: Boolean!
|
|
||||||
}
|
|
||||||
|
|
||||||
input AddEnvironmentVariableInput {
|
input AddEnvironmentVariableInput {
|
||||||
environments: [Environment!]!
|
environments: [Environment!]!
|
||||||
key: String!
|
key: String!
|
||||||
value: String!
|
value: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input AddProjectInput {
|
||||||
|
organizationId: String!
|
||||||
|
name: String!
|
||||||
|
repository: String!
|
||||||
|
prodBranch: String!
|
||||||
|
}
|
||||||
|
|
||||||
input UpdateProjectInput {
|
input UpdateProjectInput {
|
||||||
name: String
|
name: String
|
||||||
description: String
|
description: String
|
||||||
@ -188,3 +163,36 @@ input AddProjectMemberInput {
|
|||||||
input UpdateProjectMemberInput {
|
input UpdateProjectMemberInput {
|
||||||
permissions: [Permission]
|
permissions: [Permission]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
user: User!
|
||||||
|
organizations: [Organization!]
|
||||||
|
projects: [Project!]
|
||||||
|
projectsInOrganization(organizationId: String!): [Project!]
|
||||||
|
project(projectId: String!): Project
|
||||||
|
deployments(projectId: String!): [Deployment!]
|
||||||
|
environmentVariables(projectId: String!): [EnvironmentVariable!]
|
||||||
|
projectMembers(projectId: String!): [ProjectMember!]
|
||||||
|
searchProjects(searchText: String!): [Project!]
|
||||||
|
domains(projectId: String!): [Domain!]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
addProjectMember(projectId: String!, data: AddProjectMemberInput): Boolean!
|
||||||
|
updateProjectMember(projectMemberId: String!, data: UpdateProjectMemberInput): Boolean!
|
||||||
|
removeProjectMember(projectMemberId: String!): Boolean!
|
||||||
|
addEnvironmentVariables(projectId: String!, environmentVariables: [AddEnvironmentVariableInput!]): Boolean!
|
||||||
|
updateEnvironmentVariable(environmentVariableId: String!, environmentVariable: UpdateEnvironmentVariableInput!): Boolean!
|
||||||
|
removeEnvironmentVariable(environmentVariableId: String!): Boolean!
|
||||||
|
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||||
|
addProject(projectDetails: AddProjectInput): Boolean!
|
||||||
|
updateProject(projectId: String!, projectDetails: UpdateProjectInput): Boolean!
|
||||||
|
redeployToProd(deploymentId: String!): Boolean!
|
||||||
|
deleteProject(projectId: String!): Boolean!
|
||||||
|
deleteDomain(domainId: String!): Boolean!
|
||||||
|
rollbackDeployment(projectId: String!, deploymentId: String!): Boolean!
|
||||||
|
addDomain(projectId: String!, domainDetails: AddDomainInput!): Boolean!
|
||||||
|
updateDomain(domainId: String!, domainDetails: UpdateDomainInput!): Boolean!
|
||||||
|
authenticateGitHub(code: String!): AuthResult!
|
||||||
|
unauthenticateGitHub: Boolean!
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
projectsRoutesWithoutSearch,
|
projectsRoutesWithoutSearch,
|
||||||
} from './pages/projects/routes';
|
} from './pages/projects/routes';
|
||||||
import ProjectSearchLayout from './layouts/ProjectSearch';
|
import ProjectSearchLayout from './layouts/ProjectSearch';
|
||||||
|
import { OctokitProvider } from './context/OctokitContext';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -40,7 +41,11 @@ const router = createBrowserRouter([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <RouterProvider router={router} />;
|
return (
|
||||||
|
<OctokitProvider>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</OctokitProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@ -10,10 +10,10 @@ const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
|
|||||||
}&scope=${encodeURIComponent(SCOPES)}`;
|
}&scope=${encodeURIComponent(SCOPES)}`;
|
||||||
|
|
||||||
interface ConnectAccountInterface {
|
interface ConnectAccountInterface {
|
||||||
onToken: (token: string) => void;
|
onAuth: (token: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectAccount = ({ onToken }: ConnectAccountInterface) => {
|
const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
|
|
||||||
const handleCode = async (code: string) => {
|
const handleCode = async (code: string) => {
|
||||||
|
76
packages/frontend/src/components/projects/create/Deploy.tsx
Normal file
76
packages/frontend/src/components/projects/create/Deploy.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Button, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
|
import { DeployStep, DeployStatus } from './DeployStep';
|
||||||
|
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||||
|
import ConfirmDialog from '../../shared/ConfirmDialog';
|
||||||
|
|
||||||
|
const Deploy = () => {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const handleOpen = () => setOpen(!open);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
navigate('/projects/create');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h4>Deployment started ...</h4>
|
||||||
|
<div className="flex">
|
||||||
|
^
|
||||||
|
<Stopwatch
|
||||||
|
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button onClick={handleOpen} variant="outlined" size="sm">
|
||||||
|
^ Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
dialogTitle="Cancel deployment?"
|
||||||
|
handleOpen={handleOpen}
|
||||||
|
open={open}
|
||||||
|
confirmButtonTitle="Yes, Cancel deployment"
|
||||||
|
handleConfirm={handleCancel}
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
<Typography variant="small">
|
||||||
|
This will halt the deployment and you will have to start the process
|
||||||
|
from scratch.
|
||||||
|
</Typography>
|
||||||
|
</ConfirmDialog>
|
||||||
|
</div>
|
||||||
|
<DeployStep
|
||||||
|
title="Building"
|
||||||
|
status={DeployStatus.COMPLETE}
|
||||||
|
step="1"
|
||||||
|
processTime="72000"
|
||||||
|
/>
|
||||||
|
<DeployStep
|
||||||
|
title="Deployment summary"
|
||||||
|
status={DeployStatus.PROCESSING}
|
||||||
|
step="2"
|
||||||
|
startTime={Date.now().toString()}
|
||||||
|
/>
|
||||||
|
<DeployStep
|
||||||
|
title="Running checks"
|
||||||
|
status={DeployStatus.NOT_STARTED}
|
||||||
|
step="3"
|
||||||
|
/>
|
||||||
|
<DeployStep
|
||||||
|
title="Assigning domains"
|
||||||
|
status={DeployStatus.NOT_STARTED}
|
||||||
|
step="4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Deploy;
|
@ -3,9 +3,9 @@ import toast from 'react-hot-toast';
|
|||||||
|
|
||||||
import { Collapse, Button, Typography } from '@material-tailwind/react';
|
import { Collapse, Button, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
import { Stopwatch, setStopWatchOffset } from '../../../../StopWatch';
|
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||||
import FormatMillisecond from '../../../../FormatMilliSecond';
|
import FormatMillisecond from '../../FormatMilliSecond';
|
||||||
import processLogs from '../../../../../assets/process-logs.json';
|
import processLogs from '../../../assets/process-logs.json';
|
||||||
|
|
||||||
enum DeployStatus {
|
enum DeployStatus {
|
||||||
PROCESSING = 'progress',
|
PROCESSING = 'progress',
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Chip, IconButton } from '@material-tailwind/react';
|
import { Chip, IconButton } from '@material-tailwind/react';
|
||||||
|
|
||||||
@ -7,39 +8,36 @@ import { GitRepositoryDetails } from '../../../types/project';
|
|||||||
|
|
||||||
interface ProjectRepoCardProps {
|
interface ProjectRepoCardProps {
|
||||||
repository: GitRepositoryDetails;
|
repository: GitRepositoryDetails;
|
||||||
onClick: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
||||||
repository,
|
|
||||||
onClick,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Link
|
||||||
className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"
|
to={`import?owner=${repository.owner?.login}&repo=${repository.name}`}
|
||||||
onClick={onClick}
|
|
||||||
>
|
>
|
||||||
<div>^</div>
|
<div className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer">
|
||||||
<div className="grow">
|
<div>^</div>
|
||||||
<div>
|
<div className="grow">
|
||||||
<span className="text-black">{repository.full_name}</span>
|
<div>
|
||||||
{repository.visibility === 'private' ? (
|
<span className="text-black">{repository.full_name}</span>
|
||||||
<Chip
|
{repository.visibility === 'private' ? (
|
||||||
className="normal-case inline ml-6 bg-[#FED7AA] text-[#EA580C] font-normal"
|
<Chip
|
||||||
size="sm"
|
className="normal-case inline ml-6 bg-[#FED7AA] text-[#EA580C] font-normal"
|
||||||
value="Private"
|
size="sm"
|
||||||
icon={'^'}
|
value="Private"
|
||||||
/>
|
icon={'^'}
|
||||||
) : (
|
/>
|
||||||
''
|
) : (
|
||||||
)}
|
''
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p>{repository.updated_at && relativeTime(repository.updated_at)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="hidden group-hover:block">
|
||||||
|
<IconButton size="sm">{'>'}</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<p>{repository.updated_at && relativeTime(repository.updated_at)}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden group-hover:block">
|
</Link>
|
||||||
<IconButton size="sm">{'>'}</IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,14 +13,10 @@ const DEFAULT_SEARCHED_REPO = '';
|
|||||||
const REPOS_PER_PAGE = 5;
|
const REPOS_PER_PAGE = 5;
|
||||||
|
|
||||||
interface RepositoryListProps {
|
interface RepositoryListProps {
|
||||||
repoSelectionHandler: (repo: GitRepositoryDetails) => void;
|
|
||||||
octokit: Octokit;
|
octokit: Octokit;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RepositoryList = ({
|
const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||||
repoSelectionHandler,
|
|
||||||
octokit,
|
|
||||||
}: RepositoryListProps) => {
|
|
||||||
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
||||||
const [selectedAccount, setSelectedAccount] = useState('');
|
const [selectedAccount, setSelectedAccount] = useState('');
|
||||||
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
||||||
@ -135,15 +131,7 @@ const RepositoryList = ({
|
|||||||
</div>
|
</div>
|
||||||
{Boolean(repositoryDetails.length) ? (
|
{Boolean(repositoryDetails.length) ? (
|
||||||
repositoryDetails.map((repo, key) => {
|
repositoryDetails.map((repo, key) => {
|
||||||
return (
|
return <ProjectRepoCard repository={repo} key={key} />;
|
||||||
<ProjectRepoCard
|
|
||||||
repository={repo}
|
|
||||||
key={key}
|
|
||||||
onClick={() => {
|
|
||||||
repoSelectionHandler(repo);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-4 p-6 flex items-center justify-center">
|
<div className="mt-4 p-6 flex items-center justify-center">
|
||||||
|
91
packages/frontend/src/context/OctokitContext.tsx
Normal file
91
packages/frontend/src/context/OctokitContext.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
ReactNode,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
} from 'react';
|
||||||
|
import { Octokit, RequestError } from 'octokit';
|
||||||
|
|
||||||
|
import { useGQLClient } from './GQLClientContext';
|
||||||
|
|
||||||
|
const UNAUTHORIZED_ERROR_CODE = 401;
|
||||||
|
|
||||||
|
interface ContextValue {
|
||||||
|
octokit: Octokit | null;
|
||||||
|
updateAuth: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OctokitContext = createContext<ContextValue>({
|
||||||
|
octokit: null,
|
||||||
|
updateAuth: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const OctokitProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [authToken, setAuthToken] = useState<string>('');
|
||||||
|
const client = useGQLClient();
|
||||||
|
|
||||||
|
const fetchUser = useCallback(async () => {
|
||||||
|
const { user } = await client.getUser();
|
||||||
|
|
||||||
|
if (user.gitHubToken) {
|
||||||
|
setAuthToken(user.gitHubToken);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateAuth = useCallback(() => {
|
||||||
|
fetchUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const octokit = useMemo(() => {
|
||||||
|
if (!authToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Octokit({ auth: authToken });
|
||||||
|
}, [authToken]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!octokit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle React component error
|
||||||
|
const interceptor = async (error: RequestError | Error) => {
|
||||||
|
if (
|
||||||
|
error instanceof RequestError &&
|
||||||
|
error.status === UNAUTHORIZED_ERROR_CODE
|
||||||
|
) {
|
||||||
|
await client.unauthenticateGithub();
|
||||||
|
await fetchUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
};
|
||||||
|
|
||||||
|
octokit.hook.error('request', interceptor);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Remove the interceptor when the component unmounts
|
||||||
|
octokit.hook.remove('request', interceptor);
|
||||||
|
};
|
||||||
|
}, [octokit, client]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OctokitContext.Provider value={{ octokit, updateAuth }}>
|
||||||
|
{children}
|
||||||
|
</OctokitContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useOctokit = () => {
|
||||||
|
const { octokit, updateAuth } = useContext(OctokitContext);
|
||||||
|
|
||||||
|
return { octokit, updateAuth };
|
||||||
|
};
|
70
packages/frontend/src/pages/projects/create/Import.tsx
Normal file
70
packages/frontend/src/pages/projects/create/Import.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Button } from '@material-tailwind/react';
|
||||||
|
|
||||||
|
import { useOctokit } from '../../../context/OctokitContext';
|
||||||
|
import { GitRepositoryDetails } from '../../../types/project';
|
||||||
|
import Deploy from '../../../components/projects/create/Deploy';
|
||||||
|
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||||
|
|
||||||
|
const Import = () => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { octokit } = useOctokit();
|
||||||
|
const client = useGQLClient();
|
||||||
|
const [gitRepo, setGitRepo] = useState<GitRepositoryDetails>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRepo = async () => {
|
||||||
|
if (!octokit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await octokit.rest.repos.get({
|
||||||
|
owner: searchParams.get('owner') ?? '',
|
||||||
|
repo: searchParams.get('repo') ?? '',
|
||||||
|
});
|
||||||
|
|
||||||
|
setGitRepo(result.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchRepo();
|
||||||
|
}, [searchParams, octokit]);
|
||||||
|
|
||||||
|
const createProjectAndCreate = useCallback(async () => {
|
||||||
|
if (!gitRepo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { addProject } = await client.addProject({
|
||||||
|
// TODO: Implement form for setting project name
|
||||||
|
name: gitRepo.name,
|
||||||
|
// TODO: Get organization id from context or URL
|
||||||
|
organizationId: String(1),
|
||||||
|
prodBranch: gitRepo.default_branch ?? 'main',
|
||||||
|
repository: gitRepo.full_name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (addProject) {
|
||||||
|
navigate('/projects/create/success');
|
||||||
|
}
|
||||||
|
}, [client, gitRepo]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="flex w-5/6 my-4 bg-gray-200 rounded-xl p-6">
|
||||||
|
<div>^</div>
|
||||||
|
<div className="grow">{gitRepo?.full_name}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Deploy />
|
||||||
|
|
||||||
|
<Button onClick={createProjectAndCreate}>
|
||||||
|
CREATE PROJECT (FOR DEMO)
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Import;
|
@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
|
|||||||
|
|
||||||
import { Button } from '@material-tailwind/react';
|
import { Button } from '@material-tailwind/react';
|
||||||
|
|
||||||
|
// TODO: Use dynamic route params for fetching project created details
|
||||||
const Success = () => {
|
const Success = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
|
@ -8,6 +8,7 @@ const STEPPER_VALUES = [
|
|||||||
{ step: 2, route: '/projects/create/template/deploy', label: 'Deploy' },
|
{ step: 2, route: '/projects/create/template/deploy', label: 'Deploy' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// TODO: Set dynamic route for template and load details from DB
|
||||||
const CreateWithTemplate = () => {
|
const CreateWithTemplate = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ const CreateWithTemplate = () => {
|
|||||||
<div className="flex justify-between w-5/6 my-4 bg-gray-200 rounded-xl p-6">
|
<div className="flex justify-between w-5/6 my-4 bg-gray-200 rounded-xl p-6">
|
||||||
<div>^</div>
|
<div>^</div>
|
||||||
<div className="grow">React native</div>
|
<div className="grow">React native</div>
|
||||||
|
{/* TODO: Get template Git link from DB */}
|
||||||
<div>^snowball-tools/react-native-starter</div>
|
<div>^snowball-tools/react-native-starter</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 w-5/6 p-6">
|
<div className="grid grid-cols-3 w-5/6 p-6">
|
||||||
|
@ -1,66 +1,13 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React from 'react';
|
||||||
import { User } from 'gql-client';
|
|
||||||
import { Octokit, RequestError } from 'octokit';
|
|
||||||
|
|
||||||
import templateDetails from '../../../assets/templates.json';
|
import templateDetails from '../../../assets/templates.json';
|
||||||
import TemplateCard from '../../../components/projects/create/TemplateCard';
|
import TemplateCard from '../../../components/projects/create/TemplateCard';
|
||||||
import RepositoryList from '../../../components/projects/create/RepositoryList';
|
import RepositoryList from '../../../components/projects/create/RepositoryList';
|
||||||
import ConnectAccount from '../../../components/projects/create/ConnectAccount';
|
import ConnectAccount from '../../../components/projects/create/ConnectAccount';
|
||||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
import { useOctokit } from '../../../context/OctokitContext';
|
||||||
|
|
||||||
const UNAUTHORIZED_ERROR_CODE = 401;
|
|
||||||
|
|
||||||
const NewProject = () => {
|
const NewProject = () => {
|
||||||
const client = useGQLClient();
|
const { octokit, updateAuth } = useOctokit();
|
||||||
const [user, setUser] = useState<User>();
|
|
||||||
|
|
||||||
const octokit = useMemo(() => {
|
|
||||||
if (!user?.gitHubToken) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Create github/octokit context
|
|
||||||
return new Octokit({ auth: user.gitHubToken });
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
const fetchUser = useCallback(async () => {
|
|
||||||
const { user } = await client.getUser();
|
|
||||||
setUser(user);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleToken = useCallback(() => {
|
|
||||||
fetchUser();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchUser();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!octokit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Handle React component error
|
|
||||||
const interceptor = async (error: RequestError | Error) => {
|
|
||||||
if (
|
|
||||||
error instanceof RequestError &&
|
|
||||||
error.status === UNAUTHORIZED_ERROR_CODE
|
|
||||||
) {
|
|
||||||
await client.unauthenticateGithub();
|
|
||||||
await fetchUser();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
};
|
|
||||||
|
|
||||||
octokit.hook.error('request', interceptor);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
// Remove the interceptor when the component unmounts
|
|
||||||
octokit.hook.remove('request', interceptor);
|
|
||||||
};
|
|
||||||
}, [octokit, client]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -69,7 +16,7 @@ const NewProject = () => {
|
|||||||
{templateDetails.map((framework, key) => {
|
{templateDetails.map((framework, key) => {
|
||||||
return (
|
return (
|
||||||
<TemplateCard
|
<TemplateCard
|
||||||
isGitAuth={Boolean(user?.gitHubToken)}
|
isGitAuth={Boolean(octokit)}
|
||||||
framework={framework}
|
framework={framework}
|
||||||
key={key}
|
key={key}
|
||||||
/>
|
/>
|
||||||
@ -78,9 +25,9 @@ const NewProject = () => {
|
|||||||
</div>
|
</div>
|
||||||
<h5 className="mt-4 ml-4">Import a repository</h5>
|
<h5 className="mt-4 ml-4">Import a repository</h5>
|
||||||
{Boolean(octokit) ? (
|
{Boolean(octokit) ? (
|
||||||
<RepositoryList octokit={octokit!} repoSelectionHandler={() => {}} />
|
<RepositoryList octokit={octokit!} />
|
||||||
) : (
|
) : (
|
||||||
<ConnectAccount onToken={handleToken} />
|
<ConnectAccount onAuth={updateAuth} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -4,6 +4,7 @@ import NewProject from './index';
|
|||||||
import CreateWithTemplate from './Template';
|
import CreateWithTemplate from './Template';
|
||||||
import { templateRoutes } from './template/routes';
|
import { templateRoutes } from './template/routes';
|
||||||
import Success from './Success';
|
import Success from './Success';
|
||||||
|
import Import from './Import';
|
||||||
|
|
||||||
export const createProjectRoutes = [
|
export const createProjectRoutes = [
|
||||||
{
|
{
|
||||||
@ -19,4 +20,8 @@ export const createProjectRoutes = [
|
|||||||
path: 'success',
|
path: 'success',
|
||||||
element: <Success />,
|
element: <Success />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'import',
|
||||||
|
element: <Import />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
@ -1,82 +1,9 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { Button, Typography } from '@material-tailwind/react';
|
import DeployComponent from '../../../../components/projects/create/Deploy';
|
||||||
|
|
||||||
import {
|
|
||||||
DeployStep,
|
|
||||||
DeployStatus,
|
|
||||||
} from '../../../../components/projects/create/template/deploy/DeployStep';
|
|
||||||
import {
|
|
||||||
Stopwatch,
|
|
||||||
setStopWatchOffset,
|
|
||||||
} from '../../../../components/StopWatch';
|
|
||||||
import ConfirmDialog from '../../../../components/shared/ConfirmDialog';
|
|
||||||
|
|
||||||
const Deploy = () => {
|
const Deploy = () => {
|
||||||
const [open, setOpen] = React.useState(false);
|
return <DeployComponent />;
|
||||||
const handleOpen = () => setOpen(!open);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
|
||||||
navigate('/projects/create/template');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between mb-6">
|
|
||||||
<div>
|
|
||||||
<h4>Deployment started ...</h4>
|
|
||||||
<div className="flex">
|
|
||||||
^
|
|
||||||
<Stopwatch
|
|
||||||
offsetTimestamp={setStopWatchOffset(Date.now().toString())}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button onClick={handleOpen} variant="outlined" size="sm">
|
|
||||||
^ Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<ConfirmDialog
|
|
||||||
dialogTitle="Cancel deployment?"
|
|
||||||
handleOpen={handleOpen}
|
|
||||||
open={open}
|
|
||||||
confirmButtonTitle="Yes, Cancel deployment"
|
|
||||||
handleConfirm={handleCancel}
|
|
||||||
color="red"
|
|
||||||
>
|
|
||||||
<Typography variant="small">
|
|
||||||
This will halt the deployment and you will have to start the process
|
|
||||||
from scratch.
|
|
||||||
</Typography>
|
|
||||||
</ConfirmDialog>
|
|
||||||
</div>
|
|
||||||
<DeployStep
|
|
||||||
title="Building"
|
|
||||||
status={DeployStatus.COMPLETE}
|
|
||||||
step="1"
|
|
||||||
processTime="72000"
|
|
||||||
/>
|
|
||||||
<DeployStep
|
|
||||||
title="Deployment summary"
|
|
||||||
status={DeployStatus.PROCESSING}
|
|
||||||
step="2"
|
|
||||||
startTime={Date.now().toString()}
|
|
||||||
/>
|
|
||||||
<DeployStep
|
|
||||||
title="Running checks"
|
|
||||||
status={DeployStatus.NOT_STARTED}
|
|
||||||
step="3"
|
|
||||||
/>
|
|
||||||
<DeployStep
|
|
||||||
title="Assigning domains"
|
|
||||||
status={DeployStatus.NOT_STARTED}
|
|
||||||
step="4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Deploy;
|
export default Deploy;
|
||||||
|
@ -22,6 +22,8 @@ const CreateRepo = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Get users and orgs from GitHub
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(() => {})}>
|
<form onSubmit={handleSubmit(() => {})}>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
|
@ -41,6 +41,7 @@ export interface GitRepositoryDetails {
|
|||||||
owner: GitOrgDetails | null;
|
owner: GitOrgDetails | null;
|
||||||
visibility?: string;
|
visibility?: string;
|
||||||
updated_at?: string | null;
|
updated_at?: string | null;
|
||||||
|
default_branch?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GitSelect {
|
export enum GitSelect {
|
||||||
|
@ -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, AddProjectMemberInput, AddProjectMemberResponse } 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, AddProjectMemberInput, AddProjectMemberResponse, AddProjectInput, AddProjectResponse } from './types';
|
||||||
import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGitHub, unauthenticateGitHub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember, deleteDomain, addProjectMember } from './mutations';
|
import { removeProjectMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject, addDomain, rollbackDeployment, updateDomainMutation, authenticateGitHub, unauthenticateGitHub, updateEnvironmentVariable, removeEnvironmentVariable, updateProjectMember, deleteDomain, addProjectMember, addProject } from './mutations';
|
||||||
|
|
||||||
export interface GraphQLConfig {
|
export interface GraphQLConfig {
|
||||||
gqlEndpoint: string;
|
gqlEndpoint: string;
|
||||||
@ -194,6 +194,17 @@ export class GQLClient {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addProject (projectDetails: AddProjectInput): Promise<AddProjectResponse> {
|
||||||
|
const { data } = await this.client.mutate({
|
||||||
|
mutation: addProject,
|
||||||
|
variables: {
|
||||||
|
projectDetails
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
async updateProject (projectId: string, projectDetails: UpdateProjectInput): Promise<UpdateProjectResponse> {
|
async updateProject (projectId: string, projectDetails: UpdateProjectInput): Promise<UpdateProjectResponse> {
|
||||||
const { data } = await this.client.mutate({
|
const { data } = await this.client.mutate({
|
||||||
mutation: updateProjectMutation,
|
mutation: updateProjectMutation,
|
||||||
|
@ -42,6 +42,11 @@ mutation ($deploymentId: String!) {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const addProject = gql`
|
||||||
|
mutation ($projectDetails: AddProjectInput) {
|
||||||
|
addProject(projectDetails: $projectDetails)
|
||||||
|
}`;
|
||||||
|
|
||||||
export const updateProjectMutation = gql`
|
export const updateProjectMutation = gql`
|
||||||
mutation ($projectId: String!, $projectDetails: UpdateProjectInput) {
|
mutation ($projectId: String!, $projectDetails: UpdateProjectInput) {
|
||||||
updateProject(projectId: $projectId, projectDetails: $projectDetails)
|
updateProject(projectId: $projectId, projectDetails: $projectDetails)
|
||||||
|
@ -217,6 +217,10 @@ export type UpdateDeploymentToProdResponse = {
|
|||||||
updateDeploymentToProd: boolean;
|
updateDeploymentToProd: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AddProjectResponse = {
|
||||||
|
addProject: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type UpdateProjectResponse = {
|
export type UpdateProjectResponse = {
|
||||||
updateProject: boolean;
|
updateProject: boolean;
|
||||||
}
|
}
|
||||||
@ -233,6 +237,13 @@ export type DeleteDomainResponse = {
|
|||||||
deleteDomain: boolean;
|
deleteDomain: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AddProjectInput = {
|
||||||
|
organizationId: string;
|
||||||
|
name: string;
|
||||||
|
repository: string;
|
||||||
|
prodBranch: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type UpdateProjectInput = {
|
export type UpdateProjectInput = {
|
||||||
name?: string
|
name?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
Loading…
Reference in New Issue
Block a user