forked from cerc-io/snowballtools-base
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:
@@ -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;
|
||||
Reference in New Issue
Block a user