forked from cerc-io/snowballtools-base
Add GQL mutation to delete project (#43)
* Implement delete project functionality * Use delete project client method in UI * Refetch projects information on deleting project * Use project's current deployment domain name for url * Handle review changes --------- Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
parent
cfb4b4637c
commit
1ae1564878
@ -78,22 +78,39 @@ export class Database {
|
|||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectByProjectId (projectId: string): Promise<Project | null> {
|
async getProjectById (projectId: string): Promise<Project | null> {
|
||||||
const projectRepository = this.dataSource.getRepository(Project);
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
const project = await projectRepository.findOne({
|
const project = await projectRepository
|
||||||
relations: {
|
.createQueryBuilder('project')
|
||||||
organization: true,
|
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
||||||
owner: true
|
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||||
},
|
.leftJoinAndSelect('project.owner', 'owner')
|
||||||
where: {
|
.where('project.id = :projectId', {
|
||||||
id: projectId
|
projectId
|
||||||
}
|
})
|
||||||
});
|
.getOne();
|
||||||
|
|
||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProjectsInOrganization (userId: string, organizationId: string): Promise<Project[]> {
|
||||||
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
|
||||||
|
const projects = await projectRepository
|
||||||
|
.createQueryBuilder('project')
|
||||||
|
.leftJoinAndSelect('project.deployments', 'deployments', 'deployments.isCurrent = true')
|
||||||
|
.leftJoinAndSelect('deployments.domain', 'domain')
|
||||||
|
.leftJoin('project.projectMembers', 'projectMembers')
|
||||||
|
.where('(project.ownerId = :userId OR projectMembers.userId = :userId) AND project.organizationId = :organizationId', {
|
||||||
|
userId,
|
||||||
|
organizationId
|
||||||
|
})
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
return projects;
|
||||||
|
}
|
||||||
|
|
||||||
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
|
async getDeploymentsByProjectId (projectId: string): Promise<Deployment[]> {
|
||||||
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
const deploymentRepository = this.dataSource.getRepository(Deployment);
|
||||||
|
|
||||||
@ -275,4 +292,15 @@ export class Database {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteProjectById (projectId: string): Promise<boolean> {
|
||||||
|
const projectRepository = this.dataSource.getRepository(Project);
|
||||||
|
const deleteResult = await projectRepository.softDelete({ id: projectId });
|
||||||
|
|
||||||
|
if (deleteResult.affected) {
|
||||||
|
return deleteResult.affected > 0;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,14 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
OneToMany
|
OneToMany,
|
||||||
|
DeleteDateColumn
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
import { Organization } from './Organization';
|
import { Organization } from './Organization';
|
||||||
import { ProjectMember } from './ProjectMember';
|
import { ProjectMember } from './ProjectMember';
|
||||||
|
import { Deployment } from './Deployment';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Project {
|
export class Project {
|
||||||
@ -49,12 +51,21 @@ export class Project {
|
|||||||
})
|
})
|
||||||
webhooks!: string[];
|
webhooks!: string[];
|
||||||
|
|
||||||
|
@Column('varchar')
|
||||||
|
icon!: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt?: Date;
|
||||||
|
|
||||||
@OneToMany(() => ProjectMember, projectMember => projectMember.project)
|
@OneToMany(() => ProjectMember, projectMember => projectMember.project)
|
||||||
projectMembers!: ProjectMember[];
|
projectMembers!: ProjectMember[];
|
||||||
|
|
||||||
|
@OneToMany(() => Deployment, (deployment) => deployment.project)
|
||||||
|
deployments!: Deployment[];
|
||||||
}
|
}
|
||||||
|
@ -50,9 +50,14 @@ export const createResolvers = async (db: Database): Promise<any> => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
project: async (_: any, { projectId }: { projectId: string }) => {
|
project: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
const dbProject = await db.getProjectByProjectId(projectId);
|
const dbProject = await db.getProjectById(projectId);
|
||||||
|
|
||||||
return dbProject ? projectToGqlType(dbProject, [], []) : null;
|
return dbProject || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
projectsInOrganization: async (_: any, { organizationId }: {organizationId: string }, context: any) => {
|
||||||
|
const dbProject = await db.getProjectsInOrganization(context.userId, organizationId);
|
||||||
|
return dbProject;
|
||||||
},
|
},
|
||||||
|
|
||||||
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
deployments: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
@ -155,6 +160,15 @@ export const createResolvers = async (db: Database): Promise<any> => {
|
|||||||
log(err);
|
log(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteProject: async (_: any, { projectId }: { projectId: string }) => {
|
||||||
|
try {
|
||||||
|
return db.deleteProjectById(projectId);
|
||||||
|
} catch (err) {
|
||||||
|
log(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -69,6 +69,7 @@ type Project {
|
|||||||
createdAt: String!
|
createdAt: String!
|
||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
organization: Organization!
|
organization: Organization!
|
||||||
|
icon: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectMember {
|
type ProjectMember {
|
||||||
@ -115,6 +116,7 @@ type Query {
|
|||||||
user: User!
|
user: User!
|
||||||
organizations: [Organization!]
|
organizations: [Organization!]
|
||||||
projects: [Project!]
|
projects: [Project!]
|
||||||
|
projectsInOrganization(organizationId: 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!]
|
||||||
@ -128,6 +130,7 @@ type Mutation {
|
|||||||
updateDeploymentToProd(deploymentId: String!): Boolean!
|
updateDeploymentToProd(deploymentId: String!): Boolean!
|
||||||
updateProject(projectId: String!, updateProject: UpdateProjectInput): Boolean!
|
updateProject(projectId: String!, updateProject: UpdateProjectInput): Boolean!
|
||||||
redeployToProd(deploymentId: String!): Boolean!
|
redeployToProd(deploymentId: String!): Boolean!
|
||||||
|
deleteProject(projectId: String!): Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
input AddEnvironmentVariableInput {
|
input AddEnvironmentVariableInput {
|
||||||
|
35
packages/backend/test/fixtures/projects.json
vendored
35
packages/backend/test/fixtures/projects.json
vendored
@ -1,57 +1,62 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"ownerIndex":0,
|
"ownerIndex": 0,
|
||||||
"organizationIndex":0,
|
"organizationIndex": 0,
|
||||||
"name": "testProject",
|
"name": "testProject",
|
||||||
"repository": "test",
|
"repository": "test",
|
||||||
"prodBranch": "main",
|
"prodBranch": "main",
|
||||||
"description": "test",
|
"description": "test",
|
||||||
"template": "test",
|
"template": "test",
|
||||||
"framework": "test",
|
"framework": "test",
|
||||||
"webhooks": []
|
"webhooks": [],
|
||||||
|
"icon": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ownerIndex":1,
|
"ownerIndex": 1,
|
||||||
"organizationIndex":0,
|
"organizationIndex": 0,
|
||||||
"name": "testProject-2",
|
"name": "testProject-2",
|
||||||
"repository": "test-2",
|
"repository": "test-2",
|
||||||
"prodBranch": "main",
|
"prodBranch": "main",
|
||||||
"description": "test-2",
|
"description": "test-2",
|
||||||
"template": "test-2",
|
"template": "test-2",
|
||||||
"framework": "test-2",
|
"framework": "test-2",
|
||||||
"webhooks": []
|
"webhooks": [],
|
||||||
|
"icon": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ownerIndex":2,
|
"ownerIndex": 2,
|
||||||
"organizationIndex":0,
|
"organizationIndex": 0,
|
||||||
"name": "iglootools",
|
"name": "iglootools",
|
||||||
"repository": "test-3",
|
"repository": "test-3",
|
||||||
"prodBranch": "main",
|
"prodBranch": "main",
|
||||||
"description": "test-3",
|
"description": "test-3",
|
||||||
"template": "test-3",
|
"template": "test-3",
|
||||||
"framework": "test-3",
|
"framework": "test-3",
|
||||||
"webhooks": []
|
"webhooks": [],
|
||||||
|
"icon": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ownerIndex":1,
|
"ownerIndex": 1,
|
||||||
"organizationIndex":0,
|
"organizationIndex": 0,
|
||||||
"name": "iglootools-2",
|
"name": "iglootools-2",
|
||||||
"repository": "test-4",
|
"repository": "test-4",
|
||||||
"prodBranch": "main",
|
"prodBranch": "main",
|
||||||
"description": "test-4",
|
"description": "test-4",
|
||||||
"template": "test-4",
|
"template": "test-4",
|
||||||
"framework": "test-4",
|
"framework": "test-4",
|
||||||
"webhooks": []
|
"webhooks": [],
|
||||||
|
"icon": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ownerIndex":0,
|
"ownerIndex": 0,
|
||||||
"organizationIndex":1,
|
"organizationIndex": 1,
|
||||||
"name": "snowball-2",
|
"name": "snowball-2",
|
||||||
"repository": "test-5",
|
"repository": "test-5",
|
||||||
"prodBranch": "main",
|
"prodBranch": "main",
|
||||||
"description": "test-5",
|
"description": "test-5",
|
||||||
"template": "test-5",
|
"template": "test-5",
|
||||||
"framework": "test-5",
|
"framework": "test-5",
|
||||||
"webhooks": []
|
"webhooks": [],
|
||||||
|
"icon": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -25,7 +25,9 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
|
|||||||
<Link to={`projects/${project.id}`}>
|
<Link to={`projects/${project.id}`}>
|
||||||
<Typography>{project.name}</Typography>
|
<Typography>{project.name}</Typography>
|
||||||
<Typography color="gray" variant="small">
|
<Typography color="gray" variant="small">
|
||||||
{project.url}
|
{project.deployments[0]?.domain.name
|
||||||
|
? project.deployments[0]?.domain.name
|
||||||
|
: ''}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,7 +31,9 @@ const OverviewTabPanel = ({ project, organizationProject }: OverviewProps) => {
|
|||||||
<div className="grow">
|
<div className="grow">
|
||||||
<Typography>{project.name}</Typography>
|
<Typography>{project.name}</Typography>
|
||||||
<Typography variant="small" color="gray">
|
<Typography variant="small" color="gray">
|
||||||
{organizationProject.url}
|
{project.deployments[0]?.domain.name
|
||||||
|
? project.deployments[0]?.domain.name
|
||||||
|
: ''}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { Project } from 'gql-client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -11,13 +13,12 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@material-tailwind/react';
|
} from '@material-tailwind/react';
|
||||||
|
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||||
import { ProjectDetails } from '../../../../types/project';
|
|
||||||
|
|
||||||
interface DeleteProjectDialogProp {
|
interface DeleteProjectDialogProp {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
handleOpen: () => void;
|
handleOpen: () => void;
|
||||||
project: Partial<ProjectDetails>;
|
project: Project;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeleteProjectDialog = ({
|
const DeleteProjectDialog = ({
|
||||||
@ -26,6 +27,7 @@ const DeleteProjectDialog = ({
|
|||||||
project,
|
project,
|
||||||
}: DeleteProjectDialogProp) => {
|
}: DeleteProjectDialogProp) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const client = useGQLClient();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -37,6 +39,18 @@ const DeleteProjectDialog = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deleteProjectHandler = useCallback(async () => {
|
||||||
|
const { deleteProject } = await client.deleteProject(project.id);
|
||||||
|
|
||||||
|
if (deleteProject) {
|
||||||
|
navigate('/');
|
||||||
|
} else {
|
||||||
|
toast.error('Project not deleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOpen();
|
||||||
|
}, [client, project, handleOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} handler={handleOpen}>
|
<Dialog open={open} handler={handleOpen}>
|
||||||
<DialogHeader className="flex justify-between">
|
<DialogHeader className="flex justify-between">
|
||||||
@ -49,12 +63,7 @@ const DeleteProjectDialog = ({
|
|||||||
X
|
X
|
||||||
</Button>
|
</Button>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form
|
<form onSubmit={handleSubmit(deleteProjectHandler)}>
|
||||||
onSubmit={handleSubmit(() => {
|
|
||||||
handleOpen();
|
|
||||||
navigate('/');
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<DialogBody className="flex flex-col gap-2">
|
<DialogBody className="flex flex-col gap-2">
|
||||||
<Typography variant="paragraph">
|
<Typography variant="paragraph">
|
||||||
Deleting your project is irreversible. Enter your project’s
|
Deleting your project is irreversible. Enter your project’s
|
||||||
|
@ -11,14 +11,10 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
} from '@material-tailwind/react';
|
} from '@material-tailwind/react';
|
||||||
|
|
||||||
import {
|
import { ProjectDetails, RepositoryDetails } from '../../../../types/project';
|
||||||
DomainDetails,
|
|
||||||
DomainStatus,
|
|
||||||
ProjectDetails,
|
|
||||||
RepositoryDetails,
|
|
||||||
} from '../../../../types/project';
|
|
||||||
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
import ConfirmDialog from '../../../shared/ConfirmDialog';
|
||||||
import EditDomainDialog from './EditDomainDialog';
|
import EditDomainDialog from './EditDomainDialog';
|
||||||
|
import { Domain, DomainStatus } from 'gql-client';
|
||||||
|
|
||||||
enum RefreshStatus {
|
enum RefreshStatus {
|
||||||
IDLE,
|
IDLE,
|
||||||
@ -28,13 +24,20 @@ enum RefreshStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DomainCardProps {
|
interface DomainCardProps {
|
||||||
domain: DomainDetails;
|
domain: Domain;
|
||||||
repo: RepositoryDetails;
|
repo: RepositoryDetails;
|
||||||
project: ProjectDetails;
|
project: ProjectDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHECK_FAIL_TIMEOUT = 5000; // In milliseconds
|
const CHECK_FAIL_TIMEOUT = 5000; // In milliseconds
|
||||||
|
|
||||||
|
// TODO: Get domain record
|
||||||
|
const DOMAIN_RECORD = {
|
||||||
|
type: 'A',
|
||||||
|
name: '@',
|
||||||
|
value: '56.49.19.21',
|
||||||
|
};
|
||||||
|
|
||||||
const DomainCard = ({ domain, repo, project }: DomainCardProps) => {
|
const DomainCard = ({ domain, repo, project }: DomainCardProps) => {
|
||||||
const [refreshStatus, SetRefreshStatus] = useState(RefreshStatus.IDLE);
|
const [refreshStatus, SetRefreshStatus] = useState(RefreshStatus.IDLE);
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
@ -50,7 +53,7 @@ const DomainCard = ({ domain, repo, project }: DomainCardProps) => {
|
|||||||
<Chip
|
<Chip
|
||||||
className="w-fit capitalize"
|
className="w-fit capitalize"
|
||||||
value={domain.status}
|
value={domain.status}
|
||||||
color={domain.status === DomainStatus.LIVE ? 'green' : 'orange'}
|
color={domain.status === DomainStatus.Live ? 'green' : 'orange'}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon={<i>^</i>}
|
icon={<i>^</i>}
|
||||||
/>
|
/>
|
||||||
@ -106,7 +109,7 @@ const DomainCard = ({ domain, repo, project }: DomainCardProps) => {
|
|||||||
<Typography variant="small">
|
<Typography variant="small">
|
||||||
Once deleted, the project{' '}
|
Once deleted, the project{' '}
|
||||||
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
|
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
|
||||||
{project.title}
|
{project.name}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
will not be accessible from the domain{' '}
|
will not be accessible from the domain{' '}
|
||||||
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
|
<span className="bg-blue-100 rounded-sm p-0.5 text-blue-700">
|
||||||
@ -117,7 +120,7 @@ const DomainCard = ({ domain, repo, project }: DomainCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Typography variant="small">Production</Typography>
|
<Typography variant="small">Production</Typography>
|
||||||
{domain.status === DomainStatus.PENDING && (
|
{domain.status === DomainStatus.Pending && (
|
||||||
<Card className="bg-gray-200 p-4 text-sm">
|
<Card className="bg-gray-200 p-4 text-sm">
|
||||||
{refreshStatus === RefreshStatus.IDLE ? (
|
{refreshStatus === RefreshStatus.IDLE ? (
|
||||||
<Typography variant="small">
|
<Typography variant="small">
|
||||||
@ -147,9 +150,9 @@ const DomainCard = ({ domain, repo, project }: DomainCardProps) => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{domain.record.type}</td>
|
<td>{DOMAIN_RECORD.type}</td>
|
||||||
<td>{domain.record.name}</td>
|
<td>{DOMAIN_RECORD.name}</td>
|
||||||
<td>{domain.record.value}</td>
|
<td>{DOMAIN_RECORD.value}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -4,10 +4,7 @@ import { useParams, Link, useOutletContext } from 'react-router-dom';
|
|||||||
import { Button, Typography } from '@material-tailwind/react';
|
import { Button, Typography } from '@material-tailwind/react';
|
||||||
|
|
||||||
import DomainCard from './DomainCard';
|
import DomainCard from './DomainCard';
|
||||||
import {
|
import { ProjectSearchOutletContext } from '../../../../types/project';
|
||||||
DomainDetails,
|
|
||||||
ProjectSearchOutletContext,
|
|
||||||
} from '../../../../types/project';
|
|
||||||
|
|
||||||
const Domains = () => {
|
const Domains = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
@ -21,7 +18,7 @@ const Domains = () => {
|
|||||||
}, [id, projects]);
|
}, [id, projects]);
|
||||||
|
|
||||||
const linkedRepo = useMemo(() => {
|
const linkedRepo = useMemo(() => {
|
||||||
return currentProject?.repositories.find(
|
return currentProject?.repositories?.find(
|
||||||
(repo: any) => repo.id === Number(currentProject?.repositoryId),
|
(repo: any) => repo.id === Number(currentProject?.repositoryId),
|
||||||
);
|
);
|
||||||
}, [currentProject]);
|
}, [currentProject]);
|
||||||
@ -43,7 +40,7 @@ const Domains = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(domains as DomainDetails[]).map((domain) => {
|
{domains?.map((domain) => {
|
||||||
return (
|
return (
|
||||||
<DomainCard
|
<DomainCard
|
||||||
domain={domain}
|
domain={domain}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import { Domain } from 'gql-client';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -14,18 +15,14 @@ import {
|
|||||||
Option,
|
Option,
|
||||||
} from '@material-tailwind/react';
|
} from '@material-tailwind/react';
|
||||||
|
|
||||||
import {
|
import { ProjectDetails, RepositoryDetails } from '../../../../types/project';
|
||||||
DomainDetails,
|
|
||||||
ProjectDetails,
|
|
||||||
RepositoryDetails,
|
|
||||||
} from '../../../../types/project';
|
|
||||||
|
|
||||||
const DEFAULT_REDIRECT_OPTIONS = ['none'];
|
const DEFAULT_REDIRECT_OPTIONS = ['none'];
|
||||||
|
|
||||||
interface EditDomainDialogProp {
|
interface EditDomainDialogProp {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
handleOpen: () => void;
|
handleOpen: () => void;
|
||||||
domain: DomainDetails;
|
domain: Domain;
|
||||||
repo: RepositoryDetails;
|
repo: RepositoryDetails;
|
||||||
project: ProjectDetails;
|
project: ProjectDetails;
|
||||||
}
|
}
|
||||||
@ -37,7 +34,7 @@ const EditDomainDialog = ({
|
|||||||
repo,
|
repo,
|
||||||
project,
|
project,
|
||||||
}: EditDomainDialogProp) => {
|
}: EditDomainDialogProp) => {
|
||||||
const getRedirectUrl = (domain: DomainDetails) => {
|
const getRedirectUrl = (domain: Domain) => {
|
||||||
const domainArr = domain.name.split('www.');
|
const domainArr = domain.name.split('www.');
|
||||||
let redirectUrl = '';
|
let redirectUrl = '';
|
||||||
if (domain.name.startsWith('www.')) {
|
if (domain.name.startsWith('www.')) {
|
||||||
@ -79,7 +76,7 @@ const EditDomainDialog = ({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: domain.name,
|
name: domain.name,
|
||||||
branch: repo.branch[0],
|
branch: repo.branch[0],
|
||||||
redirectedTo: !domain.isRedirectedto
|
redirectedTo: !domain.isRedirected
|
||||||
? redirectOptions[0]
|
? redirectOptions[0]
|
||||||
: redirectOptions[1],
|
: redirectOptions[1],
|
||||||
},
|
},
|
||||||
|
@ -206,7 +206,7 @@ const GeneralTabPanel = ({
|
|||||||
<DeleteProjectDialog
|
<DeleteProjectDialog
|
||||||
handleOpen={handleDeleteProjectDialog}
|
handleOpen={handleDeleteProjectDialog}
|
||||||
open={openDeleteDialog}
|
open={openDeleteDialog}
|
||||||
project={{ name: 'Iglootools' }}
|
project={project}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,14 +1,41 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useOutletContext } from 'react-router-dom';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Button, Typography, Chip } from '@material-tailwind/react';
|
import { Button, Typography, Chip } from '@material-tailwind/react';
|
||||||
|
|
||||||
import ProjectCard from '../components/projects/ProjectCard';
|
import ProjectCard from '../components/projects/ProjectCard';
|
||||||
import { ProjectSearchOutletContext } from '../types/project';
|
import { useGQLClient } from '../context/GQLClientContext';
|
||||||
|
import { ProjectDetails } from '../types/project';
|
||||||
|
|
||||||
|
// TODO: Implement organization switcher
|
||||||
|
const USER_ORGANIZATION_ID = '1';
|
||||||
|
|
||||||
const Projects = () => {
|
const Projects = () => {
|
||||||
const { projects } = useOutletContext<ProjectSearchOutletContext>();
|
const client = useGQLClient();
|
||||||
|
const [projects, setProjects] = useState<ProjectDetails[]>([]);
|
||||||
|
|
||||||
|
const fetchProjects = useCallback(async () => {
|
||||||
|
const { projectsInOrganization } =
|
||||||
|
await client.getProjectsInOrganization(USER_ORGANIZATION_ID);
|
||||||
|
|
||||||
|
const updatedProjects = projectsInOrganization.map((project) => {
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
// TODO: Populate from github API
|
||||||
|
latestCommit: {
|
||||||
|
message: 'subscription added',
|
||||||
|
createdAt: '2023-12-11T04:20:00',
|
||||||
|
branch: 'main',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setProjects(updatedProjects);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProjects();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -33,7 +60,7 @@ const Projects = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-5 p-5">
|
<div className="grid grid-cols-3 gap-5 p-5">
|
||||||
{projects.length !== 0 &&
|
{projects.length !== 0 &&
|
||||||
projects.map((project: any, key: number) => {
|
projects.map((project, key) => {
|
||||||
return <ProjectCard project={project} key={key} />;
|
return <ProjectCard project={project} key={key} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,29 +1,19 @@
|
|||||||
import { Environment, EnvironmentVariable } from 'gql-client';
|
import { Environment, Project } from 'gql-client';
|
||||||
|
|
||||||
export interface ProjectDetails {
|
export interface ProjectDetails extends Project {
|
||||||
icon: string;
|
// TODO: isDomain flag
|
||||||
name: string;
|
domain?: string | null;
|
||||||
title: string;
|
// TODO: Use deployment branch
|
||||||
owner: Member;
|
source?: string;
|
||||||
organization: string;
|
|
||||||
description: string;
|
|
||||||
url: string;
|
|
||||||
domain: string | null;
|
|
||||||
id: string;
|
|
||||||
createdAt: string;
|
|
||||||
createdBy: string;
|
|
||||||
deployments: DeploymentDetails[];
|
|
||||||
source: string;
|
|
||||||
latestCommit: {
|
latestCommit: {
|
||||||
message: string;
|
message: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
branch: string;
|
branch: string;
|
||||||
};
|
};
|
||||||
repositoryId: number;
|
|
||||||
repositories: RepositoryDetails[];
|
// TODO: Move out of project
|
||||||
members: ProjectMember[];
|
repositories?: RepositoryDetails[];
|
||||||
ownerId: number;
|
repositoryId?: number;
|
||||||
environmentVariables: EnvironmentVariable[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectMember {
|
export interface ProjectMember {
|
||||||
|
@ -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 } from './queries';
|
import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject, getProjectsInOrganization } from './queries';
|
||||||
import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse } from './types';
|
import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput, RedeployToProdResponse, DeleteProjectResponse, GetProjectsInOrganizationResponse } from './types';
|
||||||
import { removeMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd } from './mutations';
|
import { removeMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation, redeployToProd, deleteProject } from './mutations';
|
||||||
|
|
||||||
export interface GraphQLConfig {
|
export interface GraphQLConfig {
|
||||||
gqlEndpoint: string;
|
gqlEndpoint: string;
|
||||||
@ -50,6 +50,17 @@ export class GQLClient {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProjectsInOrganization (organizationId: string) : Promise<GetProjectsInOrganizationResponse> {
|
||||||
|
const { data } = await this.client.query({
|
||||||
|
query: getProjectsInOrganization,
|
||||||
|
variables: {
|
||||||
|
organizationId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
async getOrganizations () : Promise<GetOrganizationsResponse> {
|
async getOrganizations () : Promise<GetOrganizationsResponse> {
|
||||||
const { data } = await this.client.query({
|
const { data } = await this.client.query({
|
||||||
query: getOrganizations
|
query: getOrganizations
|
||||||
@ -158,4 +169,15 @@ export class GQLClient {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteProject (projectId: string): Promise<DeleteProjectResponse> {
|
||||||
|
const { data } = await this.client.mutate({
|
||||||
|
mutation: deleteProject,
|
||||||
|
variables: {
|
||||||
|
projectId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,3 +28,9 @@ mutation ($deploymentId: String!) {
|
|||||||
redeployToProd(deploymentId: $deploymentId)
|
redeployToProd(deploymentId: $deploymentId)
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const deleteProject = gql`
|
||||||
|
mutation ($projectId: String!) {
|
||||||
|
deleteProject(projectId: $projectId)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -25,11 +25,69 @@ query ($projectId: String!) {
|
|||||||
framework
|
framework
|
||||||
repository
|
repository
|
||||||
webhooks
|
webhooks
|
||||||
|
icon
|
||||||
owner {
|
owner {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
email
|
email
|
||||||
}
|
}
|
||||||
|
deployments {
|
||||||
|
id
|
||||||
|
branch
|
||||||
|
isCurrent
|
||||||
|
status
|
||||||
|
title
|
||||||
|
updatedAt
|
||||||
|
commitHash
|
||||||
|
createdAt
|
||||||
|
environment
|
||||||
|
domain {
|
||||||
|
status
|
||||||
|
branch
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
id
|
||||||
|
name
|
||||||
|
isRedirected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getProjectsInOrganization = gql`
|
||||||
|
query ($organizationId: String!) {
|
||||||
|
projectsInOrganization(organizationId: $organizationId) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
createdAt
|
||||||
|
description
|
||||||
|
framework
|
||||||
|
prodBranch
|
||||||
|
webhooks
|
||||||
|
repository
|
||||||
|
updatedAt
|
||||||
|
icon
|
||||||
|
deployments {
|
||||||
|
id
|
||||||
|
branch
|
||||||
|
isCurrent
|
||||||
|
status
|
||||||
|
title
|
||||||
|
updatedAt
|
||||||
|
commitHash
|
||||||
|
createdAt
|
||||||
|
environment
|
||||||
|
domain {
|
||||||
|
status
|
||||||
|
branch
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
id
|
||||||
|
name
|
||||||
|
isRedirected
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -126,6 +126,7 @@ export type Project = {
|
|||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
organization: Organization
|
organization: Organization
|
||||||
|
icon: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetProjectMembersResponse = {
|
export type GetProjectMembersResponse = {
|
||||||
@ -156,6 +157,10 @@ export type GetProjectResponse = {
|
|||||||
project: Project | null
|
project: Project | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GetProjectsInOrganizationResponse = {
|
||||||
|
projectsInOrganization: Project[]
|
||||||
|
}
|
||||||
|
|
||||||
export type SearchProjectsResponse = {
|
export type SearchProjectsResponse = {
|
||||||
searchProjects: Project[]
|
searchProjects: Project[]
|
||||||
}
|
}
|
||||||
@ -178,6 +183,10 @@ export type UpdateProjectResponse = {
|
|||||||
updateProject: boolean;
|
updateProject: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DeleteProjectResponse = {
|
||||||
|
deleteProject: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export type UpdateProjectInput = {
|
export type UpdateProjectInput = {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
|
Loading…
Reference in New Issue
Block a user