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:
@@ -9,6 +9,7 @@ import {
|
||||
projectsRoutesWithoutSearch,
|
||||
} from './pages/projects/routes';
|
||||
import ProjectSearchLayout from './layouts/ProjectSearch';
|
||||
import { OctokitProvider } from './context/OctokitContext';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -40,7 +41,11 @@ const router = createBrowserRouter([
|
||||
]);
|
||||
|
||||
function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
return (
|
||||
<OctokitProvider>
|
||||
<RouterProvider router={router} />
|
||||
</OctokitProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -10,10 +10,10 @@ const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
|
||||
}&scope=${encodeURIComponent(SCOPES)}`;
|
||||
|
||||
interface ConnectAccountInterface {
|
||||
onToken: (token: string) => void;
|
||||
onAuth: (token: string) => void;
|
||||
}
|
||||
|
||||
const ConnectAccount = ({ onToken }: ConnectAccountInterface) => {
|
||||
const ConnectAccount = ({ onAuth: onToken }: ConnectAccountInterface) => {
|
||||
const client = useGQLClient();
|
||||
|
||||
const handleCode = async (code: string) => {
|
||||
|
||||
@@ -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
-3
@@ -3,9 +3,9 @@ import toast from 'react-hot-toast';
|
||||
|
||||
import { Collapse, Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { Stopwatch, setStopWatchOffset } from '../../../../StopWatch';
|
||||
import FormatMillisecond from '../../../../FormatMilliSecond';
|
||||
import processLogs from '../../../../../assets/process-logs.json';
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import FormatMillisecond from '../../FormatMilliSecond';
|
||||
import processLogs from '../../../assets/process-logs.json';
|
||||
|
||||
enum DeployStatus {
|
||||
PROCESSING = 'progress',
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Chip, IconButton } from '@material-tailwind/react';
|
||||
|
||||
@@ -7,39 +8,36 @@ import { GitRepositoryDetails } from '../../../types/project';
|
||||
|
||||
interface ProjectRepoCardProps {
|
||||
repository: GitRepositoryDetails;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({
|
||||
repository,
|
||||
onClick,
|
||||
}) => {
|
||||
const ProjectRepoCard: React.FC<ProjectRepoCardProps> = ({ repository }) => {
|
||||
return (
|
||||
<div
|
||||
className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer"
|
||||
onClick={onClick}
|
||||
<Link
|
||||
to={`import?owner=${repository.owner?.login}&repo=${repository.name}`}
|
||||
>
|
||||
<div>^</div>
|
||||
<div className="grow">
|
||||
<div>
|
||||
<span className="text-black">{repository.full_name}</span>
|
||||
{repository.visibility === 'private' ? (
|
||||
<Chip
|
||||
className="normal-case inline ml-6 bg-[#FED7AA] text-[#EA580C] font-normal"
|
||||
size="sm"
|
||||
value="Private"
|
||||
icon={'^'}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<div className="group flex items-center gap-4 text-gray-500 text-xs hover:bg-gray-100 p-2 cursor-pointer">
|
||||
<div>^</div>
|
||||
<div className="grow">
|
||||
<div>
|
||||
<span className="text-black">{repository.full_name}</span>
|
||||
{repository.visibility === 'private' ? (
|
||||
<Chip
|
||||
className="normal-case inline ml-6 bg-[#FED7AA] text-[#EA580C] font-normal"
|
||||
size="sm"
|
||||
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>
|
||||
<p>{repository.updated_at && relativeTime(repository.updated_at)}</p>
|
||||
</div>
|
||||
<div className="hidden group-hover:block">
|
||||
<IconButton size="sm">{'>'}</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,14 +13,10 @@ const DEFAULT_SEARCHED_REPO = '';
|
||||
const REPOS_PER_PAGE = 5;
|
||||
|
||||
interface RepositoryListProps {
|
||||
repoSelectionHandler: (repo: GitRepositoryDetails) => void;
|
||||
octokit: Octokit;
|
||||
}
|
||||
|
||||
const RepositoryList = ({
|
||||
repoSelectionHandler,
|
||||
octokit,
|
||||
}: RepositoryListProps) => {
|
||||
const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
||||
const [selectedAccount, setSelectedAccount] = useState('');
|
||||
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
||||
@@ -135,15 +131,7 @@ const RepositoryList = ({
|
||||
</div>
|
||||
{Boolean(repositoryDetails.length) ? (
|
||||
repositoryDetails.map((repo, key) => {
|
||||
return (
|
||||
<ProjectRepoCard
|
||||
repository={repo}
|
||||
key={key}
|
||||
onClick={() => {
|
||||
repoSelectionHandler(repo);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <ProjectRepoCard repository={repo} key={key} />;
|
||||
})
|
||||
) : (
|
||||
<div className="mt-4 p-6 flex items-center justify-center">
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
// TODO: Use dynamic route params for fetching project created details
|
||||
const Success = () => {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
|
||||
@@ -8,6 +8,7 @@ const STEPPER_VALUES = [
|
||||
{ step: 2, route: '/projects/create/template/deploy', label: 'Deploy' },
|
||||
];
|
||||
|
||||
// TODO: Set dynamic route for template and load details from DB
|
||||
const CreateWithTemplate = () => {
|
||||
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>^</div>
|
||||
<div className="grow">React native</div>
|
||||
{/* TODO: Get template Git link from DB */}
|
||||
<div>^snowball-tools/react-native-starter</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 w-5/6 p-6">
|
||||
|
||||
@@ -1,66 +1,13 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { User } from 'gql-client';
|
||||
import { Octokit, RequestError } from 'octokit';
|
||||
import React from 'react';
|
||||
|
||||
import templateDetails from '../../../assets/templates.json';
|
||||
import TemplateCard from '../../../components/projects/create/TemplateCard';
|
||||
import RepositoryList from '../../../components/projects/create/RepositoryList';
|
||||
import ConnectAccount from '../../../components/projects/create/ConnectAccount';
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
|
||||
const UNAUTHORIZED_ERROR_CODE = 401;
|
||||
import { useOctokit } from '../../../context/OctokitContext';
|
||||
|
||||
const NewProject = () => {
|
||||
const client = useGQLClient();
|
||||
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]);
|
||||
const { octokit, updateAuth } = useOctokit();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -69,7 +16,7 @@ const NewProject = () => {
|
||||
{templateDetails.map((framework, key) => {
|
||||
return (
|
||||
<TemplateCard
|
||||
isGitAuth={Boolean(user?.gitHubToken)}
|
||||
isGitAuth={Boolean(octokit)}
|
||||
framework={framework}
|
||||
key={key}
|
||||
/>
|
||||
@@ -78,9 +25,9 @@ const NewProject = () => {
|
||||
</div>
|
||||
<h5 className="mt-4 ml-4">Import a repository</h5>
|
||||
{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 { templateRoutes } from './template/routes';
|
||||
import Success from './Success';
|
||||
import Import from './Import';
|
||||
|
||||
export const createProjectRoutes = [
|
||||
{
|
||||
@@ -19,4 +20,8 @@ export const createProjectRoutes = [
|
||||
path: 'success',
|
||||
element: <Success />,
|
||||
},
|
||||
{
|
||||
path: 'import',
|
||||
element: <Import />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,82 +1,9 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import {
|
||||
DeployStep,
|
||||
DeployStatus,
|
||||
} from '../../../../components/projects/create/template/deploy/DeployStep';
|
||||
import {
|
||||
Stopwatch,
|
||||
setStopWatchOffset,
|
||||
} from '../../../../components/StopWatch';
|
||||
import ConfirmDialog from '../../../../components/shared/ConfirmDialog';
|
||||
import DeployComponent from '../../../../components/projects/create/Deploy';
|
||||
|
||||
const Deploy = () => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
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>
|
||||
);
|
||||
return <DeployComponent />;
|
||||
};
|
||||
|
||||
export default Deploy;
|
||||
|
||||
@@ -22,6 +22,8 @@ const CreateRepo = () => {
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Get users and orgs from GitHub
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(() => {})}>
|
||||
<div className="mb-2">
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface GitRepositoryDetails {
|
||||
owner: GitOrgDetails | null;
|
||||
visibility?: string;
|
||||
updated_at?: string | null;
|
||||
default_branch?: string;
|
||||
}
|
||||
|
||||
export enum GitSelect {
|
||||
|
||||
Reference in New Issue
Block a user