diff --git a/packages/frontend/src/components/projects/ProjectSearchBar.tsx b/packages/frontend/src/components/projects/ProjectSearchBar.tsx index bbba2d07..41c8c958 100644 --- a/packages/frontend/src/components/projects/ProjectSearchBar.tsx +++ b/packages/frontend/src/components/projects/ProjectSearchBar.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useState } from 'react'; import { useCombobox } from 'downshift'; +import { Project } from 'gql-client'; import { List, @@ -9,8 +10,6 @@ import { Typography, } from '@material-tailwind/react'; -import { Project } from 'gql-client'; - import SearchBar from '../SearchBar'; import { useGQLClient } from '../../context/GQLClientContext'; diff --git a/packages/frontend/src/components/projects/project/OverviewTabPanel.tsx b/packages/frontend/src/components/projects/project/OverviewTabPanel.tsx index c1426ef9..efbc495b 100644 --- a/packages/frontend/src/components/projects/project/OverviewTabPanel.tsx +++ b/packages/frontend/src/components/projects/project/OverviewTabPanel.tsx @@ -1,4 +1,5 @@ import React, { useMemo } from 'react'; +import { Project } from 'gql-client'; import { Typography, Button, Chip } from '@material-tailwind/react'; @@ -8,17 +9,19 @@ import { ProjectDetails } from '../../../types/project'; import { relativeTimeMs } from '../../../utils/time'; interface OverviewProps { - project: ProjectDetails; + project: Project; + organizationProject: ProjectDetails; } -const OverviewTabPanel = ({ project }: OverviewProps) => { +const OverviewTabPanel = ({ project, organizationProject }: OverviewProps) => { + // TODO: Fetch current deployment const currentDeploymentTitle = useMemo(() => { - const deployment = project.deployments.find((deployment) => { + const deployment = organizationProject?.deployments.find((deployment) => { return deployment.isCurrent === true; }); return deployment?.title; - }, []); + }, [organizationProject]); return (
@@ -28,14 +31,14 @@ const OverviewTabPanel = ({ project }: OverviewProps) => {
{project.name} - {project.url} + {organizationProject.url}
^ Domain - {!project.domain && ( + {!organizationProject.domain && ( { /> )}
- {project.domain ? ( -

{project.domain}

+ {organizationProject.domain ? ( +

{organizationProject.domain}

) : (

^ Source

-

{project.source}

+

{organizationProject.source}

^ Deployment

@@ -63,7 +66,7 @@ const OverviewTabPanel = ({ project }: OverviewProps) => {

^ Created

- {relativeTimeMs(project.createdAt)} by ^ {project.createdBy} + {relativeTimeMs(project.createdAt)} by ^ {project.owner.name}

diff --git a/packages/frontend/src/components/projects/project/ProjectTabs.tsx b/packages/frontend/src/components/projects/project/ProjectTabs.tsx index 869587fd..54327fd7 100644 --- a/packages/frontend/src/components/projects/project/ProjectTabs.tsx +++ b/packages/frontend/src/components/projects/project/ProjectTabs.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; +import { Project } from 'gql-client'; import OverviewTabPanel from './OverviewTabPanel'; import DeploymentsTabPanel from './DeploymentsTabPanel'; @@ -7,7 +8,9 @@ import { ProjectDetails } from '../../../types/project'; import SettingsTabPanel from './SettingsTabPanel'; interface ProjectTabsProps { - project: ProjectDetails; + project: Project; + organizationProject: ProjectDetails; + onUpdate: () => Promise; } const Database = () => ( @@ -28,7 +31,11 @@ const Integrations = () => ( ); -const ProjectTabs = ({ project }: ProjectTabsProps) => { +const ProjectTabs = ({ + project, + onUpdate, + organizationProject, +}: ProjectTabsProps) => { return ( { Settings - + @@ -55,7 +65,7 @@ const ProjectTabs = ({ project }: ProjectTabsProps) => { - + ); diff --git a/packages/frontend/src/components/projects/project/SettingsTabPanel.tsx b/packages/frontend/src/components/projects/project/SettingsTabPanel.tsx index 7b338ad3..d17f7686 100644 --- a/packages/frontend/src/components/projects/project/SettingsTabPanel.tsx +++ b/packages/frontend/src/components/projects/project/SettingsTabPanel.tsx @@ -1,4 +1,6 @@ import React, { createElement } from 'react'; +import { Project } from 'gql-client'; + import { Tabs, TabsHeader, @@ -46,7 +48,13 @@ const tabsData = [ }, ]; -const SettingsTabPanel = () => { +const SettingsTabPanel = ({ + project, + onUpdate, +}: { + project: Project; + onUpdate: () => Promise; +}) => { return ( <> { {tabsData.map(({ value, component }) => ( - {createElement(component)} + {createElement(component, { + project: project, + onUpdate: onUpdate, + })} ))} diff --git a/packages/frontend/src/components/projects/project/settings/EditEnvironmentVariableRow.tsx b/packages/frontend/src/components/projects/project/settings/EditEnvironmentVariableRow.tsx index 9c58d670..8d90518a 100644 --- a/packages/frontend/src/components/projects/project/settings/EditEnvironmentVariableRow.tsx +++ b/packages/frontend/src/components/projects/project/settings/EditEnvironmentVariableRow.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; import { useForm } from 'react-hook-form'; import toast from 'react-hot-toast'; -import { IconButton, Input, Typography } from '@material-tailwind/react'; - import { EnvironmentVariable } from 'gql-client'; +import { IconButton, Input, Typography } from '@material-tailwind/react'; + import ConfirmDialog from '../../../shared/ConfirmDialog'; const ShowPasswordIcon = ({ diff --git a/packages/frontend/src/components/projects/project/settings/EnvironmentVariablesTabPanel.tsx b/packages/frontend/src/components/projects/project/settings/EnvironmentVariablesTabPanel.tsx index 00171cad..c9ad0b65 100644 --- a/packages/frontend/src/components/projects/project/settings/EnvironmentVariablesTabPanel.tsx +++ b/packages/frontend/src/components/projects/project/settings/EnvironmentVariablesTabPanel.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useFieldArray, useForm } from 'react-hook-form'; import toast from 'react-hot-toast'; import { useParams } from 'react-router-dom'; +import { Environment, EnvironmentVariable } from 'gql-client'; import { Typography, @@ -12,8 +13,6 @@ import { Chip, } from '@material-tailwind/react'; -import { Environment, EnvironmentVariable } from 'gql-client'; - import AddEnvironmentVariableRow from './AddEnvironmentVariableRow'; import DisplayEnvironmentVariables from './DisplayEnvironmentVariables'; import HorizontalLine from '../../../HorizontalLine'; diff --git a/packages/frontend/src/components/projects/project/settings/GeneralTabPanel.tsx b/packages/frontend/src/components/projects/project/settings/GeneralTabPanel.tsx index 4013724b..9ea09718 100644 --- a/packages/frontend/src/components/projects/project/settings/GeneralTabPanel.tsx +++ b/packages/frontend/src/components/projects/project/settings/GeneralTabPanel.tsx @@ -1,7 +1,8 @@ -import React, { useState, useMemo } from 'react'; -import { Link, useOutletContext, useParams } from 'react-router-dom'; +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; import { useForm, Controller } from 'react-hook-form'; import toast from 'react-hot-toast'; +import { Project } from 'gql-client'; import { Button, @@ -13,7 +14,7 @@ import { import DeleteProjectDialog from './DeleteProjectDialog'; import ConfirmDialog from '../../../shared/ConfirmDialog'; -import { ProjectSearchOutletContext } from '../../../../types/project'; +import { useGQLClient } from '../../../../context/GQLClientContext'; const TEAMS = ['Airfoil']; const DEFAULT_SELECT_TEAM = undefined; @@ -32,13 +33,14 @@ const CopyIcon = ({ value }: { value: string }) => { ); }; -const GeneralTabPanel = () => { - const { id } = useParams(); - const { projects } = useOutletContext(); - - const currentProject = useMemo(() => { - return projects.find((project: any) => project.id === id); - }, [id]); +const GeneralTabPanel = ({ + project, + onUpdate, +}: { + project: Project; + onUpdate: () => Promise; +}) => { + const client = useGQLClient(); const { handleSubmit: handleTransfer, @@ -58,134 +60,155 @@ const GeneralTabPanel = () => { const handleDeleteProjectDialog = () => setOpenDeleteDialog(!openDeleteDialog); - const { handleSubmit, register } = useForm({ + const { + handleSubmit, + register, + reset, + formState: updateProjectFormState, + } = useForm({ defaultValues: { - appName: currentProject?.name, - description: currentProject?.description, + appName: project.name, + description: project.description, }, }); + useEffect(() => { + reset({ appName: project.name, description: project.description }); + }, [project]); + return ( <> - {currentProject && ( - <> -
{})}> - Project info - - App name - - - - Description (Optional) - - - - Project ID - - } - /> - -
-
- Transfer project - - Transfer this app to your personal account or a team you are a - member of. - - Learn more - - -
{ - handleTransferProjectDialog(); - })} - > - - Choose team - - ( - - )} - /> - + +
+ Transfer project + + Transfer this app to your personal account or a team you are a member + of. + + Learn more + + +
{ + handleTransferProjectDialog(); + })} + > + + Choose team + + ( + + )} + /> + + + + + Upon confirmation, your project nextjs-boilerplate will be + transferred from saugat to Airfoil. + + +
+
+ Delete project + + The project will be permanently deleted, including its deployments and + domains. This action is irreversible and can not be undone. + + + +
); }; diff --git a/packages/frontend/src/layouts/ProjectSearch.tsx b/packages/frontend/src/layouts/ProjectSearch.tsx index 43945f4d..cc35d472 100644 --- a/packages/frontend/src/layouts/ProjectSearch.tsx +++ b/packages/frontend/src/layouts/ProjectSearch.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from 'react'; import { Outlet, useNavigate } from 'react-router-dom'; - import { Environment } from 'gql-client'; -import HorizontalLine from '../components/HorizontalLine'; import { IconButton, Typography } from '@material-tailwind/react'; + +import HorizontalLine from '../components/HorizontalLine'; import ProjectSearchBar from '../components/projects/ProjectSearchBar'; import { useGQLClient } from '../context/GQLClientContext'; import { ProjectDetails } from '../types/project'; diff --git a/packages/frontend/src/pages/projects/Project.tsx b/packages/frontend/src/pages/projects/Project.tsx index 76edb514..92cd4986 100644 --- a/packages/frontend/src/pages/projects/Project.tsx +++ b/packages/frontend/src/pages/projects/Project.tsx @@ -1,27 +1,49 @@ -import React, { useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useNavigate, useOutletContext, useParams } from 'react-router-dom'; +import { Project as ProjectType } from 'gql-client'; import { Button, Typography } from '@material-tailwind/react'; import HorizontalLine from '../../components/HorizontalLine'; import ProjectTabs from '../../components/projects/project/ProjectTabs'; +import { useGQLClient } from '../../context/GQLClientContext'; import { ProjectSearchOutletContext } from '../../types/project'; const Project = () => { const { id } = useParams(); const navigate = useNavigate(); + const client = useGQLClient(); - const { projects } = useOutletContext(); + const [project, setProject] = useState(null); - const project = useMemo(() => { - return projects.find((project) => { + const fetchProject = useCallback(async (id: string | undefined) => { + if (id) { + const { project } = await client.getProject(id); + setProject(project); + } + }, []); + + useEffect(() => { + fetchProject(id); + }, [id]); + + const onUpdate = async () => { + await fetchProject(id); + }; + + // TODO: Remove organization projects + const { projects: organizationProjects } = + useOutletContext(); + + const organizationProject = useMemo(() => { + return organizationProjects.find((project) => { return project.id === id; }); - }, [id, projects]); + }, [id, organizationProjects]); return (
- {project ? ( + {project && organizationProject ? ( <>
- {project?.title} + {project?.name}
- +
) : ( diff --git a/packages/gql-client/src/client.ts b/packages/gql-client/src/client.ts index ec44be49..720b6194 100644 --- a/packages/gql-client/src/client.ts +++ b/packages/gql-client/src/client.ts @@ -1,8 +1,8 @@ import { ApolloClient, DefaultOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; -import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables } from './queries'; -import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse } from './types'; -import { removeMember, addEnvironmentVariables, updateDeploymentToProd } from './mutations'; +import { getUser, getOrganizations, getDeployments, getProjectMembers, searchProjects, getEnvironmentVariables, getProject } from './queries'; +import { AddEnvironmentVariableInput, AddEnvironmentVariablesResponse, GetDeploymentsResponse, GetEnvironmentVariablesResponse, GetOrganizationsResponse, GetProjectMembersResponse, SearchProjectsResponse, GetUserResponse, RemoveMemberResponse, UpdateDeploymentToProdResponse, GetProjectResponse, UpdateProjectResponse, UpdateProjectInput } from './types'; +import { removeMember, addEnvironmentVariables, updateDeploymentToProd, updateProjectMutation } from './mutations'; export interface GraphQLConfig { gqlEndpoint: string; @@ -39,6 +39,17 @@ export class GQLClient { return data; } + async getProject (projectId: string) : Promise { + const { data } = await this.client.query({ + query: getProject, + variables: { + projectId + } + }); + + return data; + } + async getOrganizations () : Promise { const { data } = await this.client.query({ query: getOrganizations @@ -124,4 +135,16 @@ export class GQLClient { return data; } + + async updateProject (projectId: string, updateProject: UpdateProjectInput): Promise { + const { data } = await this.client.mutate({ + mutation: updateProjectMutation, + variables: { + projectId, + updateProject + } + }); + + return data; + } } diff --git a/packages/gql-client/src/mutations.ts b/packages/gql-client/src/mutations.ts index ee2ee476..3cc81968 100644 --- a/packages/gql-client/src/mutations.ts +++ b/packages/gql-client/src/mutations.ts @@ -17,3 +17,9 @@ mutation ($deploymentId: String!) { updateDeploymentToProd(deploymentId: $deploymentId) } `; + +export const updateProjectMutation = gql` +mutation ($projectId: String!, $updateProject: UpdateProjectInput) { + updateProject(projectId: $projectId, updateProject: $updateProject) +} +`; diff --git a/packages/gql-client/src/queries.ts b/packages/gql-client/src/queries.ts index a076f0c7..507a5a45 100644 --- a/packages/gql-client/src/queries.ts +++ b/packages/gql-client/src/queries.ts @@ -12,6 +12,28 @@ query { } `; +export const getProject = gql` +query ($projectId: String!) { + project(projectId: $projectId) { + createdAt + description + id + name + template + updatedAt + prodBranch + framework + repository + webhooks + owner { + id + name + email + } + } +} +`; + export const getOrganizations = gql` query { organizations { diff --git a/packages/gql-client/src/types.ts b/packages/gql-client/src/types.ts index 4384104c..ace1ace3 100644 --- a/packages/gql-client/src/types.ts +++ b/packages/gql-client/src/types.ts @@ -152,6 +152,10 @@ export type GetUserResponse = { user: User } +export type GetProjectResponse = { + project: Project | null +} + export type SearchProjectsResponse = { searchProjects: Project[] } @@ -169,3 +173,12 @@ export type AddEnvironmentVariableInput = { export type UpdateDeploymentToProdResponse = { updateDeploymentToProd: boolean; } + +export type UpdateProjectResponse = { + updateProject: boolean; +} + +export type UpdateProjectInput = { + name: string + description: string +}