snowballtools-base/packages/frontend/src/pages/org-slug/projects/id/Overview.tsx

230 lines
8.0 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Link, useNavigate, useOutletContext } from 'react-router-dom';
import { RequestError } from 'octokit';
import { useOctokit } from 'context/OctokitContext';
import { GitCommitWithBranch, OutletContextType } from '../../../../types';
import { useGQLClient } from 'context/GQLClientContext';
import { Button, Heading, Avatar, Tag } from 'components/shared';
import { getInitials } from 'utils/geInitials';
import {
BranchStrokeIcon,
CheckRoundFilledIcon,
ClockIcon,
CursorBoxIcon,
GithubStrokeIcon,
GlobeIcon,
LinkIcon,
CalendarDaysIcon,
} from 'components/shared/CustomIcon';
import { Activity } from 'components/projects/project/overview/Activity';
import { OverviewInfo } from 'components/projects/project/overview/OverviewInfo';
import { relativeTimeMs } from 'utils/time';
import { Domain, DomainStatus } from 'gql-client';
import { AuctionCard } from 'components/projects/project/overview/Activity/AuctionCard';
const COMMITS_PER_PAGE = 4;
const OverviewTabPanel = () => {
const { octokit } = useOctokit();
const navigate = useNavigate();
const [activities, setActivities] = useState<GitCommitWithBranch[]>([]);
const [fetchingActivities, setFetchingActivities] = useState(true);
const [liveDomain, setLiveDomain] = useState<Domain>();
const client = useGQLClient();
const { project } = useOutletContext<OutletContextType>();
useEffect(() => {
setFetchingActivities(true);
// 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 () => {
try {
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);
} catch (err) {
if (!(err instanceof RequestError)) {
throw err;
}
// TODO: Show warning in activity section on request error
console.log(err.message);
} finally {
setFetchingActivities(false);
}
};
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 gap-6 md:gap-[72px]">
<div className="col-span-5 md:col-span-3">
<div className="flex items-center gap-4 mb-6">
<Avatar
size={48}
initials={getInitials(project.name)}
imageSrc={project.icon}
type="blue"
/>
<div className="flex-1 space-y-1 overflow-hidden">
<Heading className="text-lg leading-6 font-medium truncate dark:text-foreground">
{project.name}
</Heading>
{project.deployments &&
project.deployments.length > 0 &&
project.deployments.map((deployment, index) => (
<p>
<a
key={index}
href={`https://${project.name.toLowerCase()}.${deployment.deployer.baseDomain}`}
className="text-sm text-elements-low-em dark:text-foreground tracking-tight truncate"
>
{deployment.deployer.baseDomain}
</a>
</p>
))}
</div>
</div>
<OverviewInfo label="Domain" icon={<GlobeIcon />}>
{liveDomain ? (
<Tag type="positive" size="xs" leftIcon={<CheckRoundFilledIcon />}>
Connected
</Tag>
) : (
<div className="flex items-center gap-2">
<Tag type="attention" size="xs" leftIcon={<ClockIcon />}>
Not connected
</Tag>
<Button
onClick={() => {
navigate('settings/domains');
}}
variant="tertiary"
size="sm"
>
SETUP
</Button>
</div>
)}
</OverviewInfo>
{project.deployments.length !== 0 ? (
<>
{/* SOURCE */}
<OverviewInfo label="Source" icon={<GithubStrokeIcon />}>
<div className="flex gap-2 items-center">
<BranchStrokeIcon className="text-elements-low-em dark:text-foreground w-4 h-5" />
<span className="text-elements-high-em dark:text-foreground-secondary text-sm tracking-tighter">
{project.deployments[0]?.branch}
</span>
</div>
</OverviewInfo>
{/* DEPLOYMENT */}
<OverviewInfo label="Deployment URL" icon={<CursorBoxIcon />}>
{project.deployments &&
project.deployments.length > 0 &&
project.deployments.map((deployment) => (
<div className="flex gap-2 items-center">
<Link
to={`https://${project.name.toLowerCase()}.${deployment.deployer.baseDomain}`}
>
<span className="text-controls-primary dark:text-foreground group hover:border-controls-primary transition-colors border-b border-b-transparent flex gap-2 items-center text-sm tracking-tight">
{`https://${project.name.toLowerCase()}.${deployment.deployer.baseDomain}`}
<LinkIcon className="group-hover:rotate-45 transition-transform" />
</span>
</Link>
</div>
))}
</OverviewInfo>
{/* DEPLOYMENT DATE */}
<OverviewInfo label="Deployment date" icon={<CalendarDaysIcon />}>
<div className="flex gap-2 items-center text-elements-high-em dark:text-foreground text-sm tracking-tighter">
<span>{relativeTimeMs(project.deployments[0].createdAt)}</span>
by
<Avatar
// TODO: add imageSrc
// imageSrc={project.deployments[0]?.createdBy.avatar}
initials={getInitials(
project.deployments[0]?.createdBy?.name ?? '',
)}
className="rounded-full"
size={24}
/>
<span>{project.deployments[0]?.createdBy?.name}</span>
</div>
</OverviewInfo>
</>
) : (
<p className="text-elements-low-em text-sm py-3">
No current deployment found.
</p>
)}
{project.auctionId && <AuctionCard project={project} />}
</div>
<Activity activities={activities} isLoading={fetchingActivities} />
</div>
);
};
export default OverviewTabPanel;