Add GQL mutation for updating project data in general settings tab (#41)

* Use update project gql client method in UI

* Handle save project button based on form change

* Fix import order

---------

Co-authored-by: neeraj <neeraj.rtly@gmail.com>
This commit is contained in:
Nabarun Gogoi 2024-01-25 16:38:40 +05:30 committed by Ashwin Phatak
parent 61120ac44a
commit 5310d7c7d0
13 changed files with 301 additions and 166 deletions

View File

@ -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';

View File

@ -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 (
<div className="grid grid-cols-5">
@ -28,14 +31,14 @@ const OverviewTabPanel = ({ project }: OverviewProps) => {
<div className="grow">
<Typography>{project.name}</Typography>
<Typography variant="small" color="gray">
{project.url}
{organizationProject.url}
</Typography>
</div>
</div>
<div className="flex justify-between p-2 text-sm items-center">
<div>
^ Domain
{!project.domain && (
{!organizationProject.domain && (
<Chip
className="normal-case ml-6 bg-[#FED7AA] text-[#EA580C] inline font-normal"
size="sm"
@ -44,8 +47,8 @@ const OverviewTabPanel = ({ project }: OverviewProps) => {
/>
)}
</div>
{project.domain ? (
<p>{project.domain}</p>
{organizationProject.domain ? (
<p>{organizationProject.domain}</p>
) : (
<Button className="normal-case rounded-full" color="blue" size="sm">
Setup
@ -54,7 +57,7 @@ const OverviewTabPanel = ({ project }: OverviewProps) => {
</div>
<div className="flex justify-between p-2 text-sm">
<p>^ Source</p>
<p>{project.source}</p>
<p>{organizationProject.source}</p>
</div>
<div className="flex justify-between p-2 text-sm">
<p>^ Deployment</p>
@ -63,7 +66,7 @@ const OverviewTabPanel = ({ project }: OverviewProps) => {
<div className="flex justify-between p-2 text-sm">
<p>^ Created</p>
<p>
{relativeTimeMs(project.createdAt)} by ^ {project.createdBy}
{relativeTimeMs(project.createdAt)} by ^ {project.owner.name}
</p>
</div>
</div>

View File

@ -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<void>;
}
const Database = () => (
@ -28,7 +31,11 @@ const Integrations = () => (
</div>
);
const ProjectTabs = ({ project }: ProjectTabsProps) => {
const ProjectTabs = ({
project,
onUpdate,
organizationProject,
}: ProjectTabsProps) => {
return (
<Tabs
selectedTabClassName={
@ -43,7 +50,10 @@ const ProjectTabs = ({ project }: ProjectTabsProps) => {
<Tab className={'p-2 cursor-pointer'}>Settings</Tab>
</TabList>
<TabPanel>
<OverviewTabPanel project={project} />
<OverviewTabPanel
project={project}
organizationProject={organizationProject}
/>
</TabPanel>
<TabPanel>
<DeploymentsTabPanel projectId={project.id} />
@ -55,7 +65,7 @@ const ProjectTabs = ({ project }: ProjectTabsProps) => {
<Integrations />
</TabPanel>
<TabPanel>
<SettingsTabPanel />
<SettingsTabPanel project={project} onUpdate={onUpdate} />
</TabPanel>
</Tabs>
);

View File

@ -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<void>;
}) => {
return (
<>
<Tabs
@ -72,7 +80,10 @@ const SettingsTabPanel = () => {
<TabsBody className="col-span-2">
{tabsData.map(({ value, component }) => (
<TabPanel key={value} value={value} className="p-2">
{createElement(component)}
{createElement(component, {
project: project,
onUpdate: onUpdate,
})}
</TabPanel>
))}
</TabsBody>

View File

@ -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 = ({

View File

@ -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';

View File

@ -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<ProjectSearchOutletContext>();
const currentProject = useMemo(() => {
return projects.find((project: any) => project.id === id);
}, [id]);
const GeneralTabPanel = ({
project,
onUpdate,
}: {
project: Project;
onUpdate: () => Promise<void>;
}) => {
const client = useGQLClient();
const {
handleSubmit: handleTransfer,
@ -58,18 +60,35 @@ 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 && (
<>
<form onSubmit={handleSubmit(() => {})}>
<form
onSubmit={handleSubmit(async ({ appName, description }) => {
const { updateProject } = await client.updateProject(project.id, {
name: appName,
description,
});
if (updateProject) {
await onUpdate();
}
})}
>
<Typography variant="h6">Project info</Typography>
<Typography variant="small" className="font-medium text-gray-800">
App name
@ -96,20 +115,26 @@ const GeneralTabPanel = () => {
<Input
crossOrigin={undefined}
variant="outlined"
value={currentProject.id}
value={project.id}
size="md"
disabled
icon={<CopyIcon value={currentProject.id} />}
icon={<CopyIcon value={project.id} />}
/>
<Button type="submit" variant="gradient" size="sm" className="mt-1">
<Button
type="submit"
variant="gradient"
size="sm"
className="mt-1"
disabled={!updateProjectFormState.isDirty}
>
Save
</Button>
</form>
<div className="mb-1">
<Typography variant="h6">Transfer project</Typography>
<Typography variant="small">
Transfer this app to your personal account or a team you are a
member of.
Transfer this app to your personal account or a team you are a member
of.
<Link to="" className="text-blue-500">
Learn more
</Link>
@ -167,8 +192,8 @@ const GeneralTabPanel = () => {
<div className="mb-1">
<Typography variant="h6">Delete project</Typography>
<Typography variant="small">
The project will be permanently deleted, including its deployments
and domains. This action is irreversible and can not be undone.
The project will be permanently deleted, including its deployments and
domains. This action is irreversible and can not be undone.
</Typography>
<Button
variant="gradient"
@ -185,8 +210,6 @@ const GeneralTabPanel = () => {
/>
</div>
</>
)}
</>
);
};

View File

@ -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';

View File

@ -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<ProjectSearchOutletContext>();
const [project, setProject] = useState<ProjectType | null>(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<ProjectSearchOutletContext>();
const organizationProject = useMemo(() => {
return organizationProjects.find((project) => {
return project.id === id;
});
}, [id, projects]);
}, [id, organizationProjects]);
return (
<div className="h-full">
{project ? (
{project && organizationProject ? (
<>
<div className="flex p-4 gap-4 items-center">
<Button
@ -32,7 +54,7 @@ const Project = () => {
{'<'}
</Button>
<Typography variant="h3" className="grow">
{project?.title}
{project?.name}
</Typography>
<Button className="rounded-full" variant="outlined">
Open Repo
@ -43,7 +65,11 @@ const Project = () => {
</div>
<HorizontalLine />
<div className="p-4">
<ProjectTabs project={project} />
<ProjectTabs
project={project}
organizationProject={organizationProject}
onUpdate={onUpdate}
/>
</div>
</>
) : (

View File

@ -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<GetProjectResponse> {
const { data } = await this.client.query({
query: getProject,
variables: {
projectId
}
});
return data;
}
async getOrganizations () : Promise<GetOrganizationsResponse> {
const { data } = await this.client.query({
query: getOrganizations
@ -124,4 +135,16 @@ export class GQLClient {
return data;
}
async updateProject (projectId: string, updateProject: UpdateProjectInput): Promise<UpdateProjectResponse> {
const { data } = await this.client.mutate({
mutation: updateProjectMutation,
variables: {
projectId,
updateProject
}
});
return data;
}
}

View File

@ -17,3 +17,9 @@ mutation ($deploymentId: String!) {
updateDeploymentToProd(deploymentId: $deploymentId)
}
`;
export const updateProjectMutation = gql`
mutation ($projectId: String!, $updateProject: UpdateProjectInput) {
updateProject(projectId: $projectId, updateProject: $updateProject)
}
`;

View File

@ -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 {

View File

@ -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
}