Implement organization switcher and use slug in URL (#59)

* Implement dropdown for organizations switcher

* Add dynamic route for organization id

* Update routes for organization slug

* Use organization slug for adding project

* Refactor to fetch organizations at sidebar component

* Update organization switcher based on searched project

* Refactor types in frontend

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-02-07 18:41:54 +05:30 committed by GitHub
parent 6d1a48905a
commit 413ed03eb8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 293 additions and 246 deletions

View File

@ -56,6 +56,13 @@ export class Database {
return updateResult.affected > 0; return updateResult.affected > 0;
} }
async getOrganization (options: FindOneOptions<Organization>): Promise<Organization | null> {
const organizationRepository = this.dataSource.getRepository(Organization);
const organization = await organizationRepository.findOne(options);
return organization;
}
async getOrganizationsByUserId (userId: string): Promise<Organization[]> { async getOrganizationsByUserId (userId: string): Promise<Organization[]> {
const organizationRepository = this.dataSource.getRepository(Organization); const organizationRepository = this.dataSource.getRepository(Organization);
@ -108,7 +115,7 @@ export class Database {
return project; return project;
} }
async getProjectsInOrganization (userId: string, organizationId: string): Promise<Project[]> { async getProjectsInOrganization (userId: string, organizationSlug: string): Promise<Project[]> {
const projectRepository = this.dataSource.getRepository(Project); const projectRepository = this.dataSource.getRepository(Project);
const projects = await projectRepository const projects = await projectRepository
@ -116,9 +123,10 @@ export class Database {
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true') .leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
.leftJoinAndSelect('deployments.domain', 'domain') .leftJoinAndSelect('deployments.domain', 'domain')
.leftJoin('project.projectMembers', 'projectMembers') .leftJoin('project.projectMembers', 'projectMembers')
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND project.organizationId = :organizationId', { .leftJoin('project.organization', 'organization')
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND organization.slug = :organizationSlug', {
userId, userId,
organizationId organizationSlug
}) })
.getMany(); .getMany();
@ -297,7 +305,7 @@ export class Database {
return Boolean(updateResult.affected); return Boolean(updateResult.affected);
} }
async addProject (userId: string, projectDetails: DeepPartial<Project>): Promise<Project> { async addProject (userId: string, organizationId: string, projectDetails: 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
@ -312,7 +320,7 @@ export class Database {
}); });
newProject.organization = Object.assign(new Organization(), { newProject.organization = Object.assign(new Organization(), {
id: projectDetails.organizationId id: organizationId
}); });
newProject.subDomain = `${newProject.name}.${PROJECT_DOMAIN}`; newProject.subDomain = `${newProject.name}.${PROJECT_DOMAIN}`;

View File

@ -4,11 +4,13 @@ import {
Column, Column,
CreateDateColumn, CreateDateColumn,
UpdateDateColumn, UpdateDateColumn,
OneToMany OneToMany,
Unique
} from 'typeorm'; } from 'typeorm';
import { UserOrganization } from './UserOrganization'; import { UserOrganization } from './UserOrganization';
@Entity() @Entity()
@Unique(['slug'])
export class Organization { export class Organization {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@ -16,6 +18,9 @@ export class Organization {
@Column('varchar', { length: 255 }) @Column('varchar', { length: 255 })
name!: string; name!: string;
@Column('varchar')
slug!: string;
@CreateDateColumn() @CreateDateColumn()
createdAt!: Date; createdAt!: Date;

View File

@ -29,8 +29,8 @@ export const createResolvers = async (db: Database, app: OAuthApp, service: Serv
return service.getProjectById(projectId); return service.getProjectById(projectId);
}, },
projectsInOrganization: async (_: any, { organizationId }: {organizationId: string }, context: any) => { projectsInOrganization: async (_: any, { organizationSlug }: {organizationSlug: string }, context: any) => {
return service.getProjectsInOrganization(context.userId, organizationId); return service.getProjectsInOrganization(context.userId, organizationSlug);
}, },
deployments: async (_: any, { projectId }: { projectId: string }) => { deployments: async (_: any, { projectId }: { projectId: string }) => {
@ -130,9 +130,9 @@ export const createResolvers = async (db: Database, app: OAuthApp, service: Serv
} }
}, },
addProject: async (_: any, { data }: { data: DeepPartial<Project> }, context: any) => { addProject: async (_: any, { organizationSlug, data }: { organizationSlug: string, data: DeepPartial<Project> }, context: any) => {
try { try {
return service.addProject(context.userId, data); return service.addProject(context.userId, organizationSlug, data);
} catch (err) { } catch (err) {
log(err); log(err);
} }

View File

@ -41,6 +41,7 @@ type User {
type Organization { type Organization {
id: String! id: String!
name: String! name: String!
slug: String!
projects: [Project!] projects: [Project!]
createdAt: String! createdAt: String!
updatedAt: String! updatedAt: String!
@ -129,7 +130,6 @@ input AddEnvironmentVariableInput {
} }
input AddProjectInput { input AddProjectInput {
organizationId: String!
name: String! name: String!
repository: String! repository: String!
prodBranch: String! prodBranch: String!
@ -176,7 +176,7 @@ type Query {
user: User! user: User!
organizations: [Organization!] organizations: [Organization!]
projects: [Project!] projects: [Project!]
projectsInOrganization(organizationId: String!): [Project!] projectsInOrganization(organizationSlug: String!): [Project!]
project(projectId: String!): Project project(projectId: String!): Project
deployments(projectId: String!): [Deployment!] deployments(projectId: String!): [Deployment!]
environmentVariables(projectId: String!): [EnvironmentVariable!] environmentVariables(projectId: String!): [EnvironmentVariable!]
@ -193,7 +193,7 @@ type Mutation {
updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean! updateEnvironmentVariable(environmentVariableId: String!, data: UpdateEnvironmentVariableInput!): Boolean!
removeEnvironmentVariable(environmentVariableId: String!): Boolean! removeEnvironmentVariable(environmentVariableId: String!): Boolean!
updateDeploymentToProd(deploymentId: String!): Boolean! updateDeploymentToProd(deploymentId: String!): Boolean!
addProject(data: AddProjectInput): Project! addProject(organizationSlug: String!, data: AddProjectInput): Project!
updateProject(projectId: String!, projectDetails: UpdateProjectInput): Boolean! updateProject(projectId: String!, projectDetails: UpdateProjectInput): Boolean!
redeployToProd(deploymentId: String!): Boolean! redeployToProd(deploymentId: String!): Boolean!
deleteProject(projectId: String!): Boolean! deleteProject(projectId: String!): Boolean!

View File

@ -40,8 +40,8 @@ export class Service {
return dbProject; return dbProject;
} }
async getProjectsInOrganization (userId:string, organizationId: string): Promise<Project[]> { async getProjectsInOrganization (userId:string, organizationSlug: string): Promise<Project[]> {
const dbProjects = await this.db.getProjectsInOrganization(userId, organizationId); const dbProjects = await this.db.getProjectsInOrganization(userId, organizationSlug);
return dbProjects; return dbProjects;
} }
@ -182,8 +182,17 @@ export class Service {
return updateResult; return updateResult;
} }
async addProject (userId: string, data: DeepPartial<Project>): Promise<Project | undefined> { async addProject (userId: string, organizationSlug: string, data: DeepPartial<Project>): Promise<Project | undefined> {
return this.db.addProject(userId, data); const organization = await this.db.getOrganization({
where: {
slug: organizationSlug
}
});
if (!organization) {
throw new Error('Organization does not exist');
}
return this.db.addProject(userId, organization.id, data);
} }
async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> { async updateProject (projectId: string, data: DeepPartial<Project>): Promise<boolean> {

View File

@ -1,8 +1,10 @@
[ [
{ {
"name": "Snowball Tools" "name": "Snowball Tools",
"slug": "snowball-tools"
}, },
{ {
"name": "AirFoil" "name": "AirFoil",
"slug": "airfoil"
} }
] ]

View File

@ -1,26 +1,28 @@
import React from 'react'; import React from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import DashboardLayout from './layouts/Dashboard'; import OrgSlug from './pages/OrgSlug';
import Home from './pages/index'; import Projects from './pages/org-slug';
import Settings from './pages/Settings'; import Settings from './pages/org-slug/Settings';
import { import {
projectsRoutesWithSearch, projectsRoutesWithSearch,
projectsRoutesWithoutSearch, projectsRoutesWithoutSearch,
} from './pages/projects/routes'; } from './pages/org-slug/projects/routes';
import ProjectSearchLayout from './layouts/ProjectSearch'; import ProjectSearchLayout from './layouts/ProjectSearch';
import { OctokitProvider } from './context/OctokitContext'; import { OctokitProvider } from './context/OctokitContext';
import Index from './pages';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
element: <DashboardLayout />, path: ':orgSlug',
element: <OrgSlug />,
children: [ children: [
{ {
element: <ProjectSearchLayout />, element: <ProjectSearchLayout />,
children: [ children: [
{ {
path: '/', path: '',
element: <Home />, element: <Projects />,
}, },
{ {
path: 'projects', path: 'projects',
@ -38,6 +40,10 @@ const router = createBrowserRouter([
}, },
], ],
}, },
{
path: '/',
element: <Index />,
},
]); ]);
function App() { function App() {

View File

@ -1,30 +1,66 @@
import React from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Link, NavLink } from 'react-router-dom'; import { Link, NavLink, useNavigate, useParams } from 'react-router-dom';
import { Organization } from 'gql-client'; import { Organization } from 'gql-client';
import { Card, CardBody, Typography } from '@material-tailwind/react'; import { Typography, Option } from '@material-tailwind/react';
import { useGQLClient } from '../context/GQLClientContext';
import AsyncSelect from './shared/AsyncSelect';
const Sidebar = () => {
const { orgSlug } = useParams();
const navigate = useNavigate();
const client = useGQLClient();
const [selectedOrgSlug, setSelectedOrgSlug] = useState(orgSlug);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const fetchUserOrganizations = useCallback(async () => {
const { organizations } = await client.getOrganizations();
setOrganizations(organizations);
}, [orgSlug]);
useEffect(() => {
fetchUserOrganizations();
setSelectedOrgSlug(orgSlug);
}, [orgSlug]);
const Sidebar = ({ organization }: { organization: Organization }) => {
return ( return (
<div className="flex flex-col h-full p-4"> <div className="flex flex-col h-full p-4">
<div className="grow"> <div className="grow">
<div> <div>
<Link to="/"> <Link to={`/${orgSlug}`}>
<h3 className="text-black text-2xl">Snowball</h3> <h3 className="text-black text-2xl">Snowball</h3>
</Link> </Link>
</div> </div>
<Card className="-ml-1 my-2"> <AsyncSelect
<CardBody className="p-1 py-2 flex gap-2"> className="bg-white py-2"
<div>^</div> value={selectedOrgSlug}
<div> onChange={(value) => {
<Typography>{organization.name}</Typography> setSelectedOrgSlug(value!);
<Typography>Organization</Typography> navigate(`/${value}`);
}}
selected={(_, index) => (
<div className="flex gap-2">
<div>^</div>
<div>
<span>{organizations[index!]?.name}</span>
<Typography>Organization</Typography>
</div>
</div> </div>
</CardBody> )}
</Card> >
{/* TODO: Show label organization and manage in option */}
{organizations.map((org) => (
<Option key={org.id} value={org.slug}>
^ {org.name}
{org.slug === selectedOrgSlug && <p className="float-right">^</p>}
</Option>
))}
</AsyncSelect>
<div> <div>
<NavLink <NavLink
to="/" to={`/${orgSlug}`}
className={({ isActive }) => (isActive ? 'text-blue-500' : '')} className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
> >
<Typography>Projects</Typography> <Typography>Projects</Typography>
@ -32,7 +68,7 @@ const Sidebar = ({ organization }: { organization: Organization }) => {
</div> </div>
<div> <div>
<NavLink <NavLink
to="/settings" to={`/${orgSlug}/settings`}
className={({ isActive }) => (isActive ? 'text-blue-500' : '')} className={({ isActive }) => (isActive ? 'text-blue-500' : '')}
> >
<Typography>Settings</Typography> <Typography>Settings</Typography>

View File

@ -1,5 +1,5 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Button, Typography } from '@material-tailwind/react'; import { Button, Typography } from '@material-tailwind/react';
@ -11,9 +11,10 @@ const Deploy = () => {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const handleOpen = () => setOpen(!open); const handleOpen = () => setOpen(!open);
const navigate = useNavigate(); const navigate = useNavigate();
const { orgSlug } = useParams();
const handleCancel = useCallback(() => { const handleCancel = useCallback(() => {
navigate('/projects/create'); navigate(`/${orgSlug}/projects/create`);
}, []); }, []);
return ( return (

View File

@ -34,7 +34,7 @@ const TemplateCard: React.FC<TemplateCardProps> = ({
isGitAuth, isGitAuth,
}) => { }) => {
return isGitAuth ? ( return isGitAuth ? (
<Link to="/projects/create/template"> <Link to="template">
<CardDetails framework={framework} /> <CardDetails framework={framework} />
</Link> </Link>
) : ( ) : (

View File

@ -1,4 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import toast from 'react-hot-toast';
import { Environment, Project, Domain, DeploymentStatus } from 'gql-client';
import { import {
Menu, Menu,
@ -9,14 +11,12 @@ import {
Chip, Chip,
ChipProps, ChipProps,
} from '@material-tailwind/react'; } from '@material-tailwind/react';
import toast from 'react-hot-toast';
import { Environment, Project, Domain } from 'gql-client';
import { relativeTimeMs } from '../../../../utils/time'; import { relativeTimeMs } from '../../../../utils/time';
import ConfirmDialog from '../../../shared/ConfirmDialog'; import ConfirmDialog from '../../../shared/ConfirmDialog';
import DeploymentDialogBodyCard from './DeploymentDialogBodyCard'; import DeploymentDialogBodyCard from './DeploymentDialogBodyCard';
import AssignDomainDialog from './AssignDomainDialog'; import AssignDomainDialog from './AssignDomainDialog';
import { DeploymentDetails, Status } from '../../../../types/project'; import { DeploymentDetails } from '../../../../types/project';
import { useGQLClient } from '../../../../context/GQLClientContext'; import { useGQLClient } from '../../../../context/GQLClientContext';
interface DeployDetailsCardProps { interface DeployDetailsCardProps {
@ -27,10 +27,10 @@ interface DeployDetailsCardProps {
prodBranchDomains: Domain[]; prodBranchDomains: Domain[];
} }
const STATUS_COLORS: { [key in Status]: ChipProps['color'] } = { const STATUS_COLORS: { [key in DeploymentStatus]: ChipProps['color'] } = {
[Status.BUILDING]: 'blue', [DeploymentStatus.Building]: 'blue',
[Status.READY]: 'green', [DeploymentStatus.Ready]: 'green',
[Status.ERROR]: 'red', [DeploymentStatus.Error]: 'red',
}; };
const DeploymentDetailsCard = ({ const DeploymentDetailsCard = ({
@ -125,14 +125,12 @@ const DeploymentDetailsCard = ({
> >
^ Assign domain ^ Assign domain
</MenuItem> </MenuItem>
{!(deployment.environment === Environment.Production) && ( <MenuItem
<MenuItem onClick={() => setChangeToProduction(!changeToProduction)}
onClick={() => setChangeToProduction(!changeToProduction)} disabled={!(deployment.environment !== Environment.Production)}
> >
^ Change to production ^ Change to production
</MenuItem> </MenuItem>
)}
<hr className="my-3" /> <hr className="my-3" />
<MenuItem <MenuItem
onClick={() => setRedeployToProduction(!redeployToProduction)} onClick={() => setRedeployToProduction(!redeployToProduction)}
@ -145,14 +143,15 @@ const DeploymentDetailsCard = ({
> >
^ Redeploy to production ^ Redeploy to production
</MenuItem> </MenuItem>
{deployment.environment === Environment.Production && ( <MenuItem
<MenuItem onClick={() => setRollbackDeployment(!rollbackDeployment)}
onClick={() => setRollbackDeployment(!rollbackDeployment)} disabled={
disabled={deployment.isCurrent} deployment.isCurrent ||
> deployment.environment !== Environment.Production
^ Rollback to this version }
</MenuItem> >
)} ^ Rollback to this version
</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
</div> </div>

View File

@ -1,5 +1,5 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { Project } from 'gql-client'; import { Project } from 'gql-client';
@ -26,6 +26,7 @@ const DeleteProjectDialog = ({
handleOpen, handleOpen,
project, project,
}: DeleteProjectDialogProp) => { }: DeleteProjectDialogProp) => {
const { orgSlug } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const client = useGQLClient(); const client = useGQLClient();
@ -43,7 +44,7 @@ const DeleteProjectDialog = ({
const { deleteProject } = await client.deleteProject(project.id); const { deleteProject } = await client.deleteProject(project.id);
if (deleteProject) { if (deleteProject) {
navigate('/'); navigate(`/${orgSlug}`);
} else { } else {
toast.error('Project not deleted'); toast.error('Project not deleted');
} }

View File

@ -1,44 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Organization } from 'gql-client';
import { Outlet } from 'react-router-dom';
import Sidebar from '../components/Sidebar';
import { useGQLClient } from '../context/GQLClientContext';
// TODO: Implement organization switcher
// TODO: Projects get organization details through routes instead of context
const USER_ORGANIZATION_INDEX = 0;
const Dashboard = () => {
const client = useGQLClient();
const [organizations, setOrganizations] = useState<Organization[]>([]);
const fetchUserOrganizations = useCallback(async () => {
const { organizations } = await client.getOrganizations();
setOrganizations(organizations);
}, []);
useEffect(() => {
fetchUserOrganizations();
}, []);
return (
<div className="grid grid-cols-5 h-screen bg-light-blue-50">
{organizations.length > 0 && (
<>
<div className="h-full">
<Sidebar organization={organizations[USER_ORGANIZATION_INDEX]} />
</div>
<div className="col-span-4 h-full p-3 overflow-y-hidden">
<div className="bg-white rounded-3xl h-full overflow-y-auto">
<Outlet context={organizations[USER_ORGANIZATION_INDEX]} />
</div>
</div>
</>
)}
</div>
);
};
export default Dashboard;

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Outlet, useNavigate, useOutletContext } from 'react-router-dom'; import { Outlet, useNavigate } from 'react-router-dom';
import { Organization } from 'gql-client';
import { IconButton, Typography } from '@material-tailwind/react'; import { IconButton, Typography } from '@material-tailwind/react';
@ -9,7 +8,7 @@ import ProjectSearchBar from '../components/projects/ProjectSearchBar';
const ProjectSearch = () => { const ProjectSearch = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const organization = useOutletContext<Organization>();
return ( return (
<div> <div>
<div className="sticky top-0 bg-white z-30"> <div className="sticky top-0 bg-white z-30">
@ -17,7 +16,9 @@ const ProjectSearch = () => {
<div className="grow mr-2"> <div className="grow mr-2">
<ProjectSearchBar <ProjectSearchBar
onChange={(project) => { onChange={(project) => {
navigate(`/projects/${project.id}`); navigate(
`/${project.organization.slug}/projects/${project.id}`,
);
}} }}
/> />
</div> </div>
@ -34,7 +35,7 @@ const ProjectSearch = () => {
<HorizontalLine /> <HorizontalLine />
</div> </div>
<div className="z-0"> <div className="z-0">
<Outlet context={organization} /> <Outlet />
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,23 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
import Sidebar from '../components/Sidebar';
const OrgSlug = () => {
return (
<div className="grid grid-cols-5 h-screen bg-light-blue-50">
<>
<div className="h-full">
<Sidebar />
</div>
<div className="col-span-4 h-full p-3 overflow-y-hidden">
<div className="bg-white rounded-3xl h-full overflow-y-auto">
<Outlet />
</div>
</div>
</>
</div>
);
};
export default OrgSlug;

View File

@ -1,69 +1,31 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Link, useOutletContext } from 'react-router-dom'; import { Navigate } from 'react-router-dom';
import { useGQLClient } from '../context/GQLClientContext';
import { Organization } from 'gql-client'; import { Organization } from 'gql-client';
import { Button, Typography, Chip } from '@material-tailwind/react'; const Index = () => {
import ProjectCard from '../components/projects/ProjectCard';
import { useGQLClient } from '../context/GQLClientContext';
import { ProjectDetails } from '../types/project';
import { COMMIT_DETAILS } from '../constants';
const Projects = () => {
const client = useGQLClient(); const client = useGQLClient();
const organization = useOutletContext<Organization>(); const [organization, setOrganization] = useState<Organization>();
const [projects, setProjects] = useState<ProjectDetails[]>([]); const fetchUserOrganizations = useCallback(async () => {
const { organizations } = await client.getOrganizations();
const fetchProjects = useCallback(async () => { // By default information of first organization displayed
const { projectsInOrganization } = await client.getProjectsInOrganization( setOrganization(organizations[0]);
organization.id,
);
const updatedProjects = projectsInOrganization.map((project) => {
return {
...project,
// TODO: Populate from github API
latestCommit: COMMIT_DETAILS,
};
});
setProjects(updatedProjects);
}, []); }, []);
useEffect(() => { useEffect(() => {
fetchProjects(); fetchUserOrganizations();
}, []); }, []);
return ( return (
<div> <>
<div className="flex p-5"> {Boolean(organization) ? (
<div className="grow"> <Navigate to={organization!.slug} />
<div className="flex gap-2 items-center"> ) : (
<Typography variant="h4">Projects</Typography> <>Loading</>
<Chip )}
className="bg-gray-300 rounded-full static" </>
value={projects.length}
size="sm"
/>
</div>
</div>
<div>
<Link to="/projects/create">
<Button className="rounded-full" color="blue">
Create project
</Button>
</Link>
</div>
</div>
<div className="grid grid-cols-3 gap-5 p-5">
{projects.length !== 0 &&
projects.map((project, key) => {
return <ProjectCard project={project} key={key} />;
})}
</div>
</div>
); );
}; };
export default Projects; export default Index;

View File

@ -0,0 +1,67 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { Button, Typography, Chip } from '@material-tailwind/react';
import ProjectCard from '../../components/projects/ProjectCard';
import { useGQLClient } from '../../context/GQLClientContext';
import { ProjectDetails } from '../../types/project';
import { COMMIT_DETAILS } from '../../constants';
const Projects = () => {
const client = useGQLClient();
const { orgSlug } = useParams();
const [projects, setProjects] = useState<ProjectDetails[]>([]);
const fetchProjects = useCallback(async () => {
const { projectsInOrganization } = await client.getProjectsInOrganization(
orgSlug!,
);
const updatedProjects = projectsInOrganization.map((project) => {
return {
...project,
// TODO: Populate from github API
latestCommit: COMMIT_DETAILS,
};
});
setProjects(updatedProjects);
}, [orgSlug]);
useEffect(() => {
fetchProjects();
}, [orgSlug]);
return (
<div>
<div className="flex p-5">
<div className="grow">
<div className="flex gap-2 items-center">
<Typography variant="h4">Projects</Typography>
<Chip
className="bg-gray-300 rounded-full static"
value={projects.length}
size="sm"
/>
</div>
</div>
<div>
<Link to="projects/create">
<Button className="rounded-full" color="blue">
Create project
</Button>
</Link>
</div>
</div>
<div className="grid grid-cols-3 gap-5 p-5">
{projects.length !== 0 &&
projects.map((project, key) => {
return <ProjectCard project={project} key={key} />;
})}
</div>
</div>
);
};
export default Projects;

View File

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import { Outlet, Link } from 'react-router-dom'; import { Outlet, Link, useParams } from 'react-router-dom';
import { IconButton } from '@material-tailwind/react'; import { IconButton } from '@material-tailwind/react';
import HorizontalLine from '../../components/HorizontalLine'; import HorizontalLine from '../../../components/HorizontalLine';
const CreateProject = () => { const CreateProject = () => {
const { orgSlug } = useParams();
return ( return (
<div className="h-full"> <div className="h-full">
<div className="flex p-4 items-center"> <div className="flex p-4 items-center">
@ -13,7 +14,7 @@ const CreateProject = () => {
<h3 className="text-gray-750 text-2xl">Create new project</h3> <h3 className="text-gray-750 text-2xl">Create new project</h3>
</div> </div>
<div> <div>
<Link to="/"> <Link to={`/${orgSlug}`}>
<IconButton className="rounded-full" variant="outlined"> <IconButton className="rounded-full" variant="outlined">
X X
</IconButton> </IconButton>

View File

@ -4,9 +4,9 @@ import { Project as ProjectType } from 'gql-client';
import { Button, Typography } from '@material-tailwind/react'; import { Button, Typography } from '@material-tailwind/react';
import HorizontalLine from '../../components/HorizontalLine'; import HorizontalLine from '../../../components/HorizontalLine';
import ProjectTabs from '../../components/projects/project/ProjectTabs'; import ProjectTabs from '../../../components/projects/project/ProjectTabs';
import { useGQLClient } from '../../context/GQLClientContext'; import { useGQLClient } from '../../../context/GQLClientContext';
const Id = () => { const Id = () => {
const { id } = useParams(); const { id } = useParams();

View File

@ -1,15 +1,16 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { Button } from '@material-tailwind/react'; import { Button } from '@material-tailwind/react';
import { useOctokit } from '../../../context/OctokitContext'; import { useOctokit } from '../../../../context/OctokitContext';
import { GitRepositoryDetails } from '../../../types/project'; import { GitRepositoryDetails } from '../../../../types/project';
import Deploy from '../../../components/projects/create/Deploy'; import Deploy from '../../../../components/projects/create/Deploy';
import { useGQLClient } from '../../../context/GQLClientContext'; import { useGQLClient } from '../../../../context/GQLClientContext';
const Import = () => { const Import = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { orgSlug } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const { octokit } = useOctokit(); const { octokit } = useOctokit();
const client = useGQLClient(); const client = useGQLClient();
@ -37,16 +38,14 @@ const Import = () => {
return; return;
} }
const { addProject } = await client.addProject({ const { addProject } = await client.addProject(orgSlug!, {
// TODO: Implement form for setting project name // TODO: Implement form for setting project name
name: `${gitRepo.owner!.login}-${gitRepo.name}`, name: `${gitRepo.owner!.login}-${gitRepo.name}`,
// TODO: Get organization id from context or URL
organizationId: String(1),
prodBranch: gitRepo.default_branch ?? 'main', prodBranch: gitRepo.default_branch ?? 'main',
repository: gitRepo.full_name, repository: gitRepo.full_name,
}); });
navigate(`/projects/create/success/${addProject.id}`); navigate(`/${orgSlug}/projects/create/success/${addProject.id}`);
}, [client, gitRepo]); }, [client, gitRepo]);
return ( return (

View File

@ -1,7 +1,7 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Outlet, useLocation } from 'react-router-dom'; import { Outlet, useLocation } from 'react-router-dom';
import Stepper from '../../../components/Stepper'; import Stepper from '../../../../components/Stepper';
const STEPPER_VALUES = [ const STEPPER_VALUES = [
{ step: 1, route: '/projects/create/template', label: 'Create repository' }, { step: 1, route: '/projects/create/template', label: 'Create repository' },

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
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 { useOctokit } from '../../../context/OctokitContext'; import { useOctokit } from '../../../../context/OctokitContext';
const NewProject = () => { const NewProject = () => {
const { octokit, updateAuth } = useOctokit(); const { octokit, updateAuth } = useOctokit();

View File

@ -4,7 +4,7 @@ import { Link, useParams } from 'react-router-dom';
import { Button } from '@material-tailwind/react'; import { Button } from '@material-tailwind/react';
const Id = () => { const Id = () => {
const { id } = useParams(); const { id, orgSlug } = useParams();
return ( return (
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-1/2"> <div className="w-1/2">
@ -57,7 +57,7 @@ const Id = () => {
</Link> </Link>
</div> </div>
<div> <div>
<Link to={`/projects/${id}`}> <Link to={`/${orgSlug}/projects/${id}`}>
<Button className="rounded-full" variant="gradient" color="blue"> <Button className="rounded-full" variant="gradient" color="blue">
View project View project
</Button> </Button>

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import DeployComponent from '../../../../components/projects/create/Deploy'; import DeployComponent from '../../../../../components/projects/create/Deploy';
const Deploy = () => { const Deploy = () => {
return <DeployComponent />; return <DeployComponent />;

View File

@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
import { Typography } from '@material-tailwind/react'; import { Typography } from '@material-tailwind/react';
import Dropdown from '../../../../components/Dropdown'; import Dropdown from '../../../../../components/Dropdown';
const USER_OPTIONS = [ const USER_OPTIONS = [
{ value: 'saugatyadav1', label: 'saugatyadav1' }, { value: 'saugatyadav1', label: 'saugatyadav1' },
@ -93,7 +93,7 @@ const CreateRepo = () => {
</label> </label>
</div> </div>
<div className="mb-2"> <div className="mb-2">
<Link to={'/projects/create/template/deploy'}> <Link to="deploy">
<button className="bg-blue-500 rounded-xl p-2" type="submit"> <button className="bg-blue-500 rounded-xl p-2" type="submit">
Deploy ^ Deploy ^
</button> </button>

View File

@ -4,10 +4,10 @@ import toast from 'react-hot-toast';
import { Link, useParams, useSearchParams } from 'react-router-dom'; import { Link, useParams, useSearchParams } from 'react-router-dom';
import { Typography, Alert, Button } from '@material-tailwind/react'; import { Typography, Alert, Button } from '@material-tailwind/react';
import { useGQLClient } from '../../../../../context/GQLClientContext'; import { useGQLClient } from '../../../../../../context/GQLClientContext';
const Config = () => { const Config = () => {
const { id } = useParams(); const { id, orgSlug } = useParams();
const client = useGQLClient(); const client = useGQLClient();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const primaryDomainName = searchParams.get('name'); const primaryDomainName = searchParams.get('name');
@ -70,8 +70,7 @@ const Config = () => {
<i>^</i>It can take up to 48 hours for these updates to reflect <i>^</i>It can take up to 48 hours for these updates to reflect
globally. globally.
</Alert> </Alert>
<Link to={`/${orgSlug}/projects/${id}`}>
<Link to={`/projects/${id}`}>
<Button <Button
className="w-fit" className="w-fit"
color="blue" color="blue"

View File

@ -2,10 +2,10 @@ import React, { useMemo } from 'react';
import { useParams, useLocation, Outlet, Link } from 'react-router-dom'; import { useParams, useLocation, Outlet, Link } from 'react-router-dom';
import { Typography, IconButton } from '@material-tailwind/react'; import { Typography, IconButton } from '@material-tailwind/react';
import Stepper from '../../../../../components/Stepper'; import Stepper from '../../../../../../components/Stepper';
const AddDomain = () => { const AddDomain = () => {
const { id } = useParams(); const { id, orgSlug } = useParams();
const location = useLocation(); const location = useLocation();
const stepperValues = [ const stepperValues = [
@ -31,7 +31,7 @@ const AddDomain = () => {
<div className="p-4"> <div className="p-4">
<div className="flex justify-between"> <div className="flex justify-between">
<Typography variant="h3">Add Domain</Typography> <Typography variant="h3">Add Domain</Typography>
<Link to={`/projects/${id}`}> <Link to={`/${orgSlug}/projects/${id}`}>
<IconButton className="rounded-full" variant="outlined"> <IconButton className="rounded-full" variant="outlined">
X X
</IconButton> </IconButton>

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import Config from './Config'; import Config from './Config';
import SetupDomain from '../../../../../components/projects/project/settings/SetupDomain'; import SetupDomain from '../../../../../../components/projects/project/settings/SetupDomain';
export const addDomainRoutes = [ export const addDomainRoutes = [
{ {

View File

@ -2,10 +2,6 @@ import { Project, Deployment } from 'gql-client';
export interface ProjectDetails extends Project { export interface ProjectDetails extends Project {
latestCommit: Commit; latestCommit: Commit;
// TODO: Move out of project
repositories?: RepositoryDetails[];
repositoryId?: number;
} }
export interface DeploymentDetails extends Deployment { export interface DeploymentDetails extends Deployment {
@ -13,12 +9,6 @@ export interface DeploymentDetails extends Deployment {
author: string; author: string;
} }
export enum Status {
BUILDING = 'Building',
READY = 'Ready',
ERROR = 'Error',
}
export interface GitOrgDetails { export interface GitOrgDetails {
id: number; id: number;
login: string; login: string;
@ -65,27 +55,6 @@ export enum GitSelect {
NONE = 'none', NONE = 'none',
} }
export enum DomainStatus {
LIVE = 'Live',
PENDING = 'Pending',
}
export interface DomainDetails {
id: string;
projectid: string;
name: string;
status: DomainStatus;
record: {
type: string;
name: string;
value: string;
};
}
export interface ProjectSearchOutletContext {
projects: ProjectDetails[];
}
export interface Commit { export interface Commit {
message: string; message: string;
createdAt: string; createdAt: string;

View File

@ -50,11 +50,11 @@ export class GQLClient {
return data; return data;
} }
async getProjectsInOrganization (organizationId: string) : Promise<GetProjectsInOrganizationResponse> { async getProjectsInOrganization (organizationSlug: string) : Promise<GetProjectsInOrganizationResponse> {
const { data } = await this.client.query({ const { data } = await this.client.query({
query: getProjectsInOrganization, query: getProjectsInOrganization,
variables: { variables: {
organizationId organizationSlug
} }
}); });
@ -194,10 +194,11 @@ export class GQLClient {
return data; return data;
} }
async addProject (data: AddProjectInput): Promise<AddProjectResponse> { async addProject (organizationSlug: string, data: AddProjectInput): Promise<AddProjectResponse> {
const result = await this.client.mutate({ const result = await this.client.mutate({
mutation: addProject, mutation: addProject,
variables: { variables: {
organizationSlug,
data data
} }
}); });

View File

@ -43,8 +43,8 @@ mutation ($deploymentId: String!) {
`; `;
export const addProject = gql` export const addProject = gql`
mutation ($data: AddProjectInput) { mutation ($organizationSlug: String!, $data: AddProjectInput) {
addProject(data: $data) { addProject(organizationSlug: $organizationSlug, data: $data) {
id id
} }
}`; }`;

View File

@ -65,8 +65,8 @@ query ($projectId: String!) {
`; `;
export const getProjectsInOrganization = gql` export const getProjectsInOrganization = gql`
query ($organizationId: String!) { query ($organizationSlug: String!) {
projectsInOrganization(organizationId: $organizationId) { projectsInOrganization(organizationSlug: $organizationSlug) {
id id
name name
createdAt createdAt
@ -106,6 +106,7 @@ query {
organizations { organizations {
id id
name name
slug
createdAt createdAt
updatedAt updatedAt
} }
@ -191,6 +192,7 @@ query ($searchText: String!) {
organization { organization {
id id
name name
slug
createdAt createdAt
updatedAt updatedAt
} }

View File

@ -109,6 +109,7 @@ export type OrganizationProject = {
export type Organization = { export type Organization = {
id: string id: string
name: string name: string
slug: string
projects: OrganizationProject[] projects: OrganizationProject[]
createdAt: string createdAt: string
updatedAt: string updatedAt: string
@ -240,7 +241,6 @@ export type DeleteDomainResponse = {
} }
export type AddProjectInput = { export type AddProjectInput = {
organizationId: string;
name: string; name: string;
repository: string; repository: string;
prodBranch: string; prodBranch: string;