Added deployments overview
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { makeApiUrl } from '.';
|
||||
import type { Deployment } from './types/deployments';
|
||||
|
||||
export const getDeploymentsForSite = async (
|
||||
siteId: string,
|
||||
limit: number = 100
|
||||
): Promise<Deployment[]> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/deployments?limit=${limit}`), {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch deployments');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.deployments;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
export type DeploymentStatus = 'pending' | 'running' | 'success' | 'failed';
|
||||
|
||||
export interface Deployment {
|
||||
id: string;
|
||||
site_id: string;
|
||||
commit_hash: string;
|
||||
message: string;
|
||||
status: DeploymentStatus;
|
||||
start_time: string;
|
||||
finish_time: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface GetDeploymentsBySiteResponse {
|
||||
deployments: Deployment[];
|
||||
total: number;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { CheckCircle2, Clock, Loader2, XCircle } from 'lucide-react';
|
||||
import type { DeploymentStatus } from '../api/types/deployments';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
const StatusIcon = ({ status }: { status: DeploymentStatus }) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return <CheckCircle2 className="w-4 h-4 text-success" />;
|
||||
case 'failed':
|
||||
return <XCircle className="w-4 h-4 text-destructive" />;
|
||||
case 'running':
|
||||
return <Loader2 className="w-4 h-4 text-primary animate-spin" />;
|
||||
case 'pending':
|
||||
return <Clock className="w-4 h-4 text-muted-foreground" />;
|
||||
}
|
||||
};
|
||||
|
||||
const DeploymentStatusBadge = ({ status }: { status: DeploymentStatus }) => {
|
||||
const statusText = status.charAt(0).toUpperCase() + status.slice(1);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIcon status={status} />
|
||||
<span
|
||||
className={cn(
|
||||
'text-sm capitalize hidden sm:inline',
|
||||
status === 'success' && 'text-success',
|
||||
status === 'failed' && 'text-destructive',
|
||||
status === 'running' && 'text-primary',
|
||||
status === 'pending' && 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{statusText}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeploymentStatusBadge;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getDeploymentsForSite } from '../../api/deployments.api';
|
||||
|
||||
export function useDeploymentsForSite(siteId: string, limit: number) {
|
||||
return useQuery({
|
||||
queryKey: ['deployments', siteId, limit],
|
||||
queryFn: async () => getDeploymentsForSite(siteId, limit),
|
||||
enabled: !!siteId,
|
||||
});
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import { useSites } from '../../hooks/api/useSites';
|
||||
import type { Site } from '../../api/types/site';
|
||||
import { Skeleton } from '../../components/ui/skeleton';
|
||||
import { memo } from 'react';
|
||||
import { useDeploymentsForSite } from '../../hooks/api/useDeploymentsForSite';
|
||||
import { formatDateRelativeOrAbsolute } from '../../utils/dateTime';
|
||||
|
||||
const SiteAvatar = ({ name, enabled }: { name: string; enabled: boolean }) => (
|
||||
<div
|
||||
@@ -28,6 +30,8 @@ const SiteAvatar = ({ name, enabled }: { name: string; enabled: boolean }) => (
|
||||
);
|
||||
|
||||
const SiteRow = ({ site }: { site: Site }) => {
|
||||
const { data: deployments } = useDeploymentsForSite(site.id, 1);
|
||||
|
||||
return (
|
||||
<Link to={`/sites/${site.id}`}>
|
||||
<Card className="flex flex-row items-center justify-between px-4 py-3 cursor-pointer hover:border-border transition-colors">
|
||||
@@ -47,11 +51,14 @@ const SiteRow = ({ site }: { site: Site }) => {
|
||||
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="text-right hidden sm:block">
|
||||
{/* TODO: Replace with actual deployment time */}
|
||||
<p className="text-[11px] uppercase tracking-wide text-muted-foreground mb-0.5">
|
||||
Last deployed
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">1 hour ago</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{deployments && deployments.length > 0
|
||||
? formatDateRelativeOrAbsolute(deployments[0].created_at)
|
||||
: '—'}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={site.enabled ? 'default' : 'secondary'} className="gap-1.5">
|
||||
{site.enabled ? <Check /> : <X />}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CheckCircle2, Clock, Loader2, RotateCcw, XCircle } from 'lucide-react';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
@@ -15,24 +15,12 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '../../components/ui/table';
|
||||
import type { MockDeploymentStatus, MockSite } from './SiteOverview';
|
||||
import { formatDate } from '../../utils/formatDate';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { formatDateRelativeOrAbsolute, formatDuration } from '../../utils/dateTime';
|
||||
import type { Deployment } from '../../api/types/deployments';
|
||||
import DeploymentStatusBadge from '../../components/DeploymentStatusBadge';
|
||||
import type { Site } from '../../api/types/site';
|
||||
|
||||
const StatusIcon = ({ status }: { status: MockDeploymentStatus }) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return <CheckCircle2 className="w-4 h-4 text-success" />;
|
||||
case 'failed':
|
||||
return <XCircle className="w-4 h-4 text-destructive" />;
|
||||
case 'building':
|
||||
return <Loader2 className="w-4 h-4 text-primary animate-spin" />;
|
||||
case 'queued':
|
||||
return <Clock className="w-4 h-4 text-muted-foreground" />;
|
||||
}
|
||||
};
|
||||
|
||||
const DeploymentsTab = ({ site }: { site: MockSite }) => (
|
||||
const DeploymentsTab = ({ deployments, site }: { deployments?: Deployment[]; site: Site }) => (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div>
|
||||
@@ -55,33 +43,36 @@ const DeploymentsTab = ({ site }: { site: MockSite }) => (
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{site.deployments.map((d) => (
|
||||
<TableRow key={d.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIcon status={d.status} />
|
||||
<span
|
||||
className={cn(
|
||||
'text-sm capitalize hidden sm:inline',
|
||||
d.status === 'success' && 'text-success',
|
||||
d.status === 'failed' && 'text-destructive',
|
||||
d.status === 'building' && 'text-primary',
|
||||
d.status === 'queued' && 'text-muted-foreground'
|
||||
)}
|
||||
{deployments?.map((d) => {
|
||||
const githubCommitUrl = `https://github.com/${site.owner}/${site.repository}/commit/${d.commit_hash}`;
|
||||
return (
|
||||
<TableRow key={d.id}>
|
||||
<TableCell>
|
||||
<DeploymentStatusBadge status={d.status} />
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
<a
|
||||
href={githubCommitUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{d.status}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">{d.commit}</TableCell>
|
||||
<TableCell className="hidden sm:table-cell text-muted-foreground">
|
||||
{d.message}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-muted-foreground">
|
||||
{formatDate(d.timestamp)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{d.commit_hash.substring(0, 7)}
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell text-muted-foreground">
|
||||
{d.message}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-muted-foreground">
|
||||
{d.start_time && d.finish_time
|
||||
? formatDuration(d.start_time, d.finish_time, false)
|
||||
: '—'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-muted-foreground">
|
||||
{formatDateRelativeOrAbsolute(d.created_at)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
|
||||
@@ -8,15 +8,24 @@ import {
|
||||
CardDescription,
|
||||
} from '../../components/ui/card';
|
||||
import { Separator } from '../../components/ui/separator';
|
||||
import { formatDate } from '../../utils/formatDate';
|
||||
import { Table, TableBody, TableHead, TableHeader, TableRow } from '../../components/ui/table';
|
||||
import { formatDateRelativeOrAbsolute, formatDuration } from '../../utils/dateTime';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '../../components/ui/table';
|
||||
import type { Deployment } from '../../api/types/deployments';
|
||||
import DeploymentStatusBadge from '../../components/DeploymentStatusBadge';
|
||||
|
||||
const repoUrl = (site: Site) => {
|
||||
const host = site.git_server === 'github' ? 'github.com' : 'gitlab.com';
|
||||
return `https://${host}/${site.owner}/${site.repository}`;
|
||||
};
|
||||
|
||||
const OverviewTab = ({ site }: { site: Site }) => (
|
||||
const OverviewTab = ({ site, deployments }: { site: Site; deployments?: Deployment[] }) => (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -79,7 +88,11 @@ const OverviewTab = ({ site }: { site: Site }) => (
|
||||
<Separator />
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Last Deployed</span>
|
||||
<span>{formatDate('2024-01-01T12:00:00Z')}</span>
|
||||
<span>
|
||||
{deployments
|
||||
? formatDateRelativeOrAbsolute(deployments[0]?.created_at)
|
||||
: '—'}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -90,32 +103,51 @@ const OverviewTab = ({ site }: { site: Site }) => (
|
||||
<CardDescription>Last 3 deployments for this site.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Commit</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">Message</TableHead>
|
||||
<TableHead className="text-right">Time</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{/* {site.deployments.slice(0, 3).map((d) => (
|
||||
<TableRow key={d.id}>
|
||||
<TableCell>
|
||||
<StatusIcon status={d.status} />
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">{d.commit}</TableCell>
|
||||
<TableCell className="hidden sm:table-cell text-muted-foreground">
|
||||
{d.message}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-muted-foreground">
|
||||
{formatDate(d.timestamp)}
|
||||
</TableCell>
|
||||
{deployments && deployments.length > 0 && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Commit</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">Message</TableHead>
|
||||
<TableHead className="text-right">Duration</TableHead>
|
||||
<TableHead className="text-right">Time</TableHead>
|
||||
</TableRow>
|
||||
))} */}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{deployments.slice(0, 3).map((d) => {
|
||||
const githubCommitUrl = `https://github.com/${site.owner}/${site.repository}/commit/${d.commit_hash}`;
|
||||
return (
|
||||
<TableRow key={d.id}>
|
||||
<TableCell>
|
||||
<DeploymentStatusBadge status={d.status} />
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
<a
|
||||
href={githubCommitUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{d.commit_hash.substring(0, 7)}
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell text-muted-foreground">
|
||||
{d.message}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-muted-foreground">
|
||||
{d.start_time && d.finish_time
|
||||
? formatDuration(d.start_time, d.finish_time, false)
|
||||
: '—'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-muted-foreground">
|
||||
{formatDateRelativeOrAbsolute(d.created_at)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -27,73 +27,7 @@ import {
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from '../../components/ui/empty';
|
||||
|
||||
export type MockDeploymentStatus = 'success' | 'failed' | 'building' | 'queued';
|
||||
|
||||
export interface MockDeployment {
|
||||
id: string;
|
||||
commit: string;
|
||||
message: string;
|
||||
status: MockDeploymentStatus;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface MockSite {
|
||||
id: string;
|
||||
name: string;
|
||||
gitServer: 'github' | 'gitlab';
|
||||
owner: string;
|
||||
repository: string;
|
||||
branch: string;
|
||||
domain: string;
|
||||
spa: boolean;
|
||||
enabled: boolean;
|
||||
lastDeployed: string;
|
||||
deployments: MockDeployment[];
|
||||
}
|
||||
|
||||
const MOCK_SITE: MockSite = {
|
||||
id: '1',
|
||||
name: 'My Portfolio',
|
||||
gitServer: 'github',
|
||||
owner: 'janedoe',
|
||||
repository: 'portfolio',
|
||||
branch: 'gh-pages',
|
||||
domain: 'janedoe.dev',
|
||||
spa: true,
|
||||
enabled: true,
|
||||
lastDeployed: '2026-04-06T10:32:00Z',
|
||||
deployments: [
|
||||
{
|
||||
id: 'd1',
|
||||
commit: 'a3f8c21',
|
||||
message: 'Update hero section copy',
|
||||
status: 'success',
|
||||
timestamp: '2026-04-06T10:32:00Z',
|
||||
},
|
||||
{
|
||||
id: 'd2',
|
||||
commit: 'b7e1d44',
|
||||
message: 'Add project cards component',
|
||||
status: 'success',
|
||||
timestamp: '2026-04-05T16:18:00Z',
|
||||
},
|
||||
{
|
||||
id: 'd3',
|
||||
commit: 'c92fa08',
|
||||
message: 'Fix broken image paths',
|
||||
status: 'failed',
|
||||
timestamp: '2026-04-05T14:05:00Z',
|
||||
},
|
||||
{
|
||||
id: 'd4',
|
||||
commit: 'de610bb',
|
||||
message: 'Initial commit',
|
||||
status: 'success',
|
||||
timestamp: '2026-04-04T09:00:00Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
import { useDeploymentsForSite } from '../../hooks/api/useDeploymentsForSite';
|
||||
|
||||
const VALID_TABS = [
|
||||
'overview',
|
||||
@@ -163,8 +97,7 @@ const SiteOverview = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { data: site, isLoading: isLoadingSite, error: siteError } = useSite(id!);
|
||||
const toggleSiteEnabled = useToggleSiteEnabled(id!);
|
||||
|
||||
const mockSite = MOCK_SITE;
|
||||
const { data: deployments } = useDeploymentsForSite(id!, 100);
|
||||
|
||||
const tabParam = searchParams.get('tab');
|
||||
const activeTab: TabValue =
|
||||
@@ -266,11 +199,11 @@ const SiteOverview = () => {
|
||||
</div>
|
||||
|
||||
<TabsContent value="overview">
|
||||
<OverviewTab site={site} />
|
||||
<OverviewTab site={site} deployments={deployments} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="deployments">
|
||||
<DeploymentsTab site={mockSite} />
|
||||
<DeploymentsTab site={site} deployments={deployments} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="settings">
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
export const formatDate = (iso: string) => {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
export const formatRelativeDate = (iso: string) => {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffSeconds = Math.floor(diffMs / 1000);
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffSeconds < 60) return `${diffSeconds} seconds ago`;
|
||||
if (diffMinutes < 60) return `${diffMinutes} minutes ago`;
|
||||
if (diffHours < 24) return `${diffHours} hours ago`;
|
||||
return `${diffDays} days ago`;
|
||||
};
|
||||
|
||||
export const formatDuration = (startIso: string, endIso: string, full: boolean = false) => {
|
||||
if (!startIso || !endIso) return '—';
|
||||
const start = new Date(startIso);
|
||||
const end = new Date(endIso);
|
||||
console.log('Calculating duration between', start, 'and', end);
|
||||
const diffMs = end.getTime() - start.getTime();
|
||||
const diffSeconds = Math.floor(diffMs / 1000);
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
|
||||
if (diffSeconds < 60) return `${diffSeconds}${full ? ' seconds' : 's'}`;
|
||||
if (diffMinutes < 60) return `${diffMinutes}${full ? ' minutes' : 'm'}`;
|
||||
return `${diffHours}${full ? ' hours' : 'h'}`;
|
||||
};
|
||||
|
||||
export const formatDateRelativeOrAbsolute = (iso: string) => {
|
||||
const d = new Date(iso);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays < 7) {
|
||||
return formatRelativeDate(iso);
|
||||
} else {
|
||||
return formatDate(iso);
|
||||
}
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
export const formatDate = (iso: string) => {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user