Implement routes for project tabs (#63)

* Add routes to project tabs

* remove react tabs and use material tailwind component instead

* Refactor code to move project tab panels in pages directory

* Remove unused function from database class

* Refactor routes for project tabs
This commit is contained in:
2024-02-12 12:17:47 +05:30
committed by GitHub
parent a58b9b255e
commit 559e0f8934
11 changed files with 152 additions and 136 deletions
@@ -1,131 +0,0 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Project, Domain } from 'gql-client';
import { Button, Typography } from '@material-tailwind/react';
import DeploymentDetailsCard from './deployments/DeploymentDetailsCard';
import FilterForm, {
FilterValue,
StatusOptions,
} from './deployments/FilterForm';
import { DeploymentDetails } from '../../../types/project';
import { useGQLClient } from '../../../context/GQLClientContext';
import { COMMIT_DETAILS } from '../../../constants';
const DEFAULT_FILTER_VALUE: FilterValue = {
searchedBranch: '',
status: StatusOptions.ALL_STATUS,
};
const DeploymentsTabPanel = ({ project }: { project: Project }) => {
const client = useGQLClient();
const [filterValue, setFilterValue] = useState(DEFAULT_FILTER_VALUE);
const [deployments, setDeployments] = useState<DeploymentDetails[]>([]);
const [prodBranchDomains, setProdBranchDomains] = useState<Domain[]>([]);
const fetchDeployments = async () => {
const { deployments } = await client.getDeployments(project.id);
const updatedDeployments = deployments.map((deployment) => {
return {
...deployment,
author: '',
commit: COMMIT_DETAILS,
};
});
setDeployments(updatedDeployments);
};
const fetchProductionBranchDomains = useCallback(async () => {
const { domains } = await client.getDomains(project.id, {
branch: project.prodBranch,
});
setProdBranchDomains(domains);
}, []);
useEffect(() => {
fetchDeployments();
fetchProductionBranchDomains();
}, []);
const currentDeployment = useMemo(() => {
return deployments.find((deployment) => {
return deployment.isCurrent === true;
});
}, [deployments]);
const filteredDeployments = useMemo<DeploymentDetails[]>(() => {
return deployments.filter((deployment) => {
// Searched branch filter
const branchMatch =
!filterValue.searchedBranch ||
deployment.branch
.toLowerCase()
.includes(filterValue.searchedBranch.toLowerCase());
// Status filter
const statusMatch =
filterValue.status === StatusOptions.ALL_STATUS ||
// TODO: match status field types
(deployment.status as unknown as StatusOptions) === filterValue.status;
const dateMatch =
!filterValue.updateAtRange ||
(new Date(deployment.updatedAt) >= filterValue.updateAtRange!.from! &&
new Date(deployment.updatedAt) <= filterValue.updateAtRange!.to!);
return branchMatch && statusMatch && dateMatch;
}) as DeploymentDetails[];
}, [filterValue, deployments]);
const handleResetFilters = useCallback(() => {
setFilterValue(DEFAULT_FILTER_VALUE);
}, []);
const onUpdateDeploymenToProd = async () => {
await fetchDeployments();
};
return (
<div className="p-4">
<FilterForm
value={filterValue}
onChange={(value) => setFilterValue(value)}
/>
<div className="mt-2">
{Boolean(filteredDeployments.length) ? (
filteredDeployments.map((deployment, key) => {
return (
<DeploymentDetailsCard
deployment={deployment}
key={key}
currentDeployment={currentDeployment!}
onUpdate={onUpdateDeploymenToProd}
project={project}
prodBranchDomains={prodBranchDomains}
/>
);
})
) : (
<div className="h-[50vh] bg-gray-100 flex rounded items-center justify-center">
<div className="text-center">
<Typography variant="h5">No deployments found</Typography>
<Typography>
Please change your search query or filters
</Typography>
<Button
className="rounded-full mt-5"
color="white"
onClick={handleResetFilters}
>
^ Reset filters
</Button>
</div>
</div>
)}
</div>
</div>
);
};
export default DeploymentsTabPanel;
@@ -1,178 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Domain, DomainStatus, Project } from 'gql-client';
import { Typography, Button, Chip } from '@material-tailwind/react';
import ActivityCard from './ActivityCard';
import { relativeTimeMs } from '../../../utils/time';
import { useOctokit } from '../../../context/OctokitContext';
import { GitCommitDetails } from '../../../types/project';
import { useGQLClient } from '../../../context/GQLClientContext';
const COMMITS_PER_PAGE = 4;
interface OverviewProps {
project: Project;
}
// TODO: Check if any live domain is set for production branch
const OverviewTabPanel = ({ project }: OverviewProps) => {
const { octokit } = useOctokit();
const [activities, setActivities] = useState<GitCommitDetails[]>([]);
const [liveDomain, setLiveDomain] = useState<Domain>();
const client = useGQLClient();
useEffect(() => {
if (!octokit) {
return;
}
// TODO: Save repo commits in DB and avoid using GitHub API in frontend
// TODO: Figure out fetching latest commits for all branches
const fetchRepoActivity = async () => {
const [owner, repo] = project.repository.split('/');
if (!repo) {
// Do not fetch branches if repo not available
return;
}
// Get all branches in project repo
const result = await octokit.rest.repos.listBranches({
owner,
repo,
});
// Get first 4 commits from repo branches
const commitsByBranchPromises = result.data.map(async (branch) => {
const result = await octokit.rest.repos.listCommits({
owner,
repo,
sha: branch.commit.sha,
per_page: COMMITS_PER_PAGE,
});
return result.data.map((data) => ({
...data,
branch,
}));
});
const commitsByBranch = await Promise.all(commitsByBranchPromises);
const commitsWithBranch = commitsByBranch.flat();
// Order commits by date and set latest 4 commits in activity section
const orderedCommits = commitsWithBranch
.sort(
(a, b) =>
new Date(b.commit.author!.date!).getTime() -
new Date(a.commit.author!.date!).getTime(),
)
.slice(0, COMMITS_PER_PAGE);
setActivities(orderedCommits);
};
fetchRepoActivity();
}, [octokit, project]);
useEffect(() => {
const fetchLiveProdDomain = async () => {
const { domains } = await client.getDomains(project.id, {
branch: project.prodBranch,
status: DomainStatus.Live,
});
if (domains.length === 0) {
return;
}
setLiveDomain(domains[0]);
};
fetchLiveProdDomain();
}, [project]);
return (
<div className="grid grid-cols-5">
<div className="col-span-3 p-2">
<div className="flex items-center gap-2 p-2 ">
<div>^</div>
<div className="grow">
<Typography>{project.name}</Typography>
<Typography variant="small" color="gray">
{project.subDomain}
</Typography>
</div>
</div>
<div className="flex justify-between p-2 text-sm items-center">
<div>^ Domain</div>
{liveDomain ? (
<Chip
className="normal-case ml-6 inline font-normal"
size="sm"
value="Connected"
icon="^"
color="green"
/>
) : (
<div className="flex justify-between items-center w-full m-2">
<Chip
className="normal-case inline font-normal"
size="sm"
value="Not connected"
icon="^"
color="orange"
/>
<Button
className="normal-case rounded-full"
color="blue"
size="sm"
>
Setup
</Button>
</div>
)}
</div>
{project.deployments.length !== 0 ? (
<>
<div className="flex justify-between p-2 text-sm">
<p>^ Source</p>
<p>{project.deployments[0]?.branch}</p>
</div>
<div className="flex justify-between p-2 text-sm">
<p>^ Deployment</p>
<p className="text-blue-600">{liveDomain?.name}</p>
</div>
<div className="flex justify-between p-2 text-sm">
<p>^ Created</p>
<p>
{relativeTimeMs(project.deployments[0].createdAt)} by ^{' '}
{project.deployments[0].createdBy.name}
</p>
</div>
</>
) : (
<div>No current deployment found</div>
)}
</div>
<div className="col-span-2 p-2">
<div className="flex justify-between">
<Typography variant="h6">Activity</Typography>
<button className="text-xs bg-gray-300 rounded-full p-2">
See all
</button>
</div>
<div className="p-2">
{activities.map((activity, index) => {
return <ActivityCard activity={activity} key={index} />;
})}
</div>
</div>
</div>
);
};
export default OverviewTabPanel;
@@ -1,65 +0,0 @@
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';
import SettingsTabPanel from './SettingsTabPanel';
interface ProjectTabsProps {
project: Project;
onUpdate: () => Promise<void>;
}
const Database = () => (
<div>
Content of database tab
<p className="block">
It is a long established fact that a reader will be distracted by the
readable content of a page when looking at its layout.
</p>
</div>
);
const Integrations = () => (
<div>
Content of integrations tab
<p className="block">
There are many variations of passages of Lorem Ipsum available.
</p>
</div>
);
const ProjectTabs = ({ project, onUpdate }: ProjectTabsProps) => {
return (
<Tabs
selectedTabClassName={
'border-b-2 border-gray-900 text-gray-900 focus:outline-none'
}
>
<TabList className="flex border-b border-gray-300 text-gray-600">
<Tab className={'p-2 cursor-pointer'}>Overview</Tab>
<Tab className={'p-2 cursor-pointer'}>Deployments</Tab>
<Tab className={'p-2 cursor-pointer'}>Database</Tab>
<Tab className={'p-2 cursor-pointer'}>Integrations</Tab>
<Tab className={'p-2 cursor-pointer'}>Settings</Tab>
</TabList>
<TabPanel>
<OverviewTabPanel project={project} />
</TabPanel>
<TabPanel>
<DeploymentsTabPanel project={project} />
</TabPanel>
<TabPanel>
<Database />
</TabPanel>
<TabPanel>
<Integrations />
</TabPanel>
<TabPanel>
<SettingsTabPanel project={project} onUpdate={onUpdate} />
</TabPanel>
</Tabs>
);
};
export default ProjectTabs;
@@ -1,95 +0,0 @@
import React, { createElement } from 'react';
import { Project } from 'gql-client';
import {
Tabs,
TabsHeader,
TabsBody,
Tab,
TabPanel,
} from '@material-tailwind/react';
import Domains from './settings/Domains';
import GeneralTabPanel from './settings/GeneralTabPanel';
import { EnvironmentVariablesTabPanel } from './settings/EnvironmentVariablesTabPanel';
import GitTabPanel from './settings/GitTabPanel';
import MembersTabPanel from './settings/MembersTabPanel';
const tabsData = [
{
label: 'General',
icon: '^',
value: 'general',
component: GeneralTabPanel,
},
{
label: 'Domains',
icon: '^',
value: 'domains',
component: Domains,
},
{
label: 'Git',
icon: '^',
value: 'git',
component: GitTabPanel,
},
{
label: 'Environment variables',
icon: '^',
value: 'environmentVariables',
component: EnvironmentVariablesTabPanel,
},
{
label: 'Members',
icon: '^',
value: 'members',
component: MembersTabPanel,
},
];
const SettingsTabPanel = ({
project,
onUpdate,
}: {
project: Project;
onUpdate: () => Promise<void>;
}) => {
return (
<>
<Tabs
value={'general'}
orientation="vertical"
className="grid grid-cols-4"
>
<TabsHeader
className="bg-transparent col-span-1"
indicatorProps={{
className: 'bg-gray-900/10 shadow-none !text-gray-900',
}}
>
{tabsData.map(({ label, value, icon }) => (
<Tab key={value} value={value} className="flex justify-start">
<div className="flex gap-2">
<div>{icon}</div>
<div>{label}</div>
</div>
</Tab>
))}
</TabsHeader>
<TabsBody className="col-span-2">
{tabsData.map(({ value, component }) => (
<TabPanel key={value} value={value} className="p-2">
{createElement(component, {
project: project,
onUpdate: onUpdate,
})}
</TabPanel>
))}
</TabsBody>
</Tabs>
</>
);
};
export default SettingsTabPanel;