Add frontend #1

Merged
KartoffelChipss merged 50 commits from feature/frontend into main 2026-05-06 20:16:59 +02:00
6 changed files with 253 additions and 187 deletions
Showing only changes of commit c3f4c95150 - Show all commits
@@ -0,0 +1,20 @@
import { Code2 } from 'lucide-react';
import type { GitServerType } from '../api/types/gitserver';
import { GiteaIcon } from './icons/GiteaIcon';
import { GitHubIcon } from './icons/GitHubIcon';
import { GitLabIcon } from './icons/GitLabIcon';
const GitServerTypeIcon = ({ type, size = 32 }: { type: GitServerType; size?: number }) => {
switch (type) {
case 'github':
return <GitHubIcon size={size} />;
case 'gitlab':
return <GitLabIcon size={size} />;
case 'gitea':
return <GiteaIcon size={size} />;
default:
return <Code2 />;
}
};
export default GitServerTypeIcon;
@@ -10,6 +10,9 @@ export function useDeleteGitServer() {
qc.invalidateQueries({ qc.invalidateQueries({
queryKey: ['gitservers'], queryKey: ['gitservers'],
}); });
qc.invalidateQueries({
queryKey: ['gitServer'],
});
}, },
}); });
} }
@@ -0,0 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import { getGitServerById } from '../../../api/gitservers.api';
export function useGitServer(id: string) {
return useQuery({
queryKey: ['gitServer', id],
queryFn: () => getGitServerById(id),
});
}
+4 -19
View File
@@ -33,9 +33,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '../../components/ui/select'; } from '../../components/ui/select';
import { GiteaIcon } from '../../components/icons/GiteaIcon'; import GitServerTypeIcon from '../../components/GitServerTypeIcon';
import { GitLabIcon } from '../../components/icons/GitLabIcon';
import { GitHubIcon } from '../../components/icons/GitHubIcon';
const DeleteGitServerDialog = ({ gitServerId }: { gitServerId: string }) => { const DeleteGitServerDialog = ({ gitServerId }: { gitServerId: string }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -60,8 +58,8 @@ const DeleteGitServerDialog = ({ gitServerId }: { gitServerId: string }) => {
<DialogHeader> <DialogHeader>
<DialogTitle>Delete git server</DialogTitle> <DialogTitle>Delete git server</DialogTitle>
<DialogDescription> <DialogDescription>
Are you sure you want to delete this git server? This action cannot be Are you sure you want to delete this git server? This will delete all
undone. associated sites and cannot be undone.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
@@ -81,24 +79,11 @@ const DeleteGitServerDialog = ({ gitServerId }: { gitServerId: string }) => {
); );
}; };
const GitServerTypeIcon = ({ type }: { type: GitServerType }) => {
switch (type) {
case 'github':
return <GitHubIcon size={32} />;
case 'gitlab':
return <GitLabIcon size={32} />;
case 'gitea':
return <GiteaIcon size={32} />;
default:
return <Code2 />;
}
};
const GitServerRow = ({ gitServer }: { gitServer: GitServer }) => { const GitServerRow = ({ gitServer }: { gitServer: GitServer }) => {
return ( return (
<div className="flex items-center gap-4 p-4 rounded-lg border"> <div className="flex items-center gap-4 p-4 rounded-lg border">
<div className="text-2xl"> <div className="text-2xl">
<GitServerTypeIcon type={gitServer.type} /> <GitServerTypeIcon type={gitServer.type} size={32} />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<p className="font-medium">{gitServer.name}</p> <p className="font-medium">{gitServer.name}</p>
+69 -41
View File
@@ -17,47 +17,32 @@ import {
import { Plus, Zap, Copy, Check, AlertTriangle } from 'lucide-react'; import { Plus, Zap, Copy, Check, AlertTriangle } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useCreateSite } from '../../hooks/api/useCreateSite'; import { useCreateSite } from '../../hooks/api/useCreateSite';
import { useNavigate } from 'react-router'; import { Link, useNavigate } from 'react-router';
import GitServerTypeIcon from '../../components/GitServerTypeIcon';
import { useGitServers } from '../../hooks/api/gitservers/useGitServers';
const GIT_SERVERS = [ interface ParsedRepo {
{ gitServer: string;
value: 'github', owner: string;
label: 'GitHub', repository: string;
icon: ( }
<svg viewBox="0 0 24 24" className="w-6 h-6 fill-current">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
</svg>
),
},
{
value: 'gitlab',
label: 'GitLab',
icon: (
<svg viewBox="0 0 24 24" className="w-6 h-6 fill-current">
<path d="M23.955 13.587l-1.342-4.135-2.664-8.189a.455.455 0 00-.867 0L16.418 9.45H7.582L4.918 1.263a.455.455 0 00-.867 0L1.387 9.452.045 13.587a.924.924 0 00.331 1.023L12 23.054l11.624-8.443a.92.92 0 00.331-1.024" />
</svg>
),
},
];
const parseRepoUrl = (url: string) => { const parseRepoUrl = (url: string): ParsedRepo | null => {
const cleaned = url const cleaned = url
.trim() .trim()
.replace(/\.git$/, '') .replace(/\.git$/, '')
.replace(/\/$/, ''); .replace(/\/$/, '');
const patterns = [ // HTTPS: https://host/owner/repo
/^https?:\/\/(github\.com|gitlab\.com)\/([^/]+)\/([^/]+)/, const httpsMatch = cleaned.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+)/);
/^git@(github\.com|gitlab\.com):([^/]+)\/([^/]+)/, if (httpsMatch) {
]; return { gitServer: httpsMatch[1], owner: httpsMatch[2], repository: httpsMatch[3] };
}
for (const pattern of patterns) { // SSH: git@host:owner/repo
const match = cleaned.match(pattern); const sshMatch = cleaned.match(/^git@([^:]+):([^/]+)\/([^/]+)/);
if (match) { if (sshMatch) {
const host = match[1]; return { gitServer: sshMatch[1], owner: sshMatch[2], repository: sshMatch[3] };
const server = host === 'github.com' ? 'github' : 'gitlab';
return { gitServer: server, owner: match[2], repository: match[3] };
}
} }
return null; return null;
@@ -80,13 +65,28 @@ const NewSite = () => {
const [copiedToken, setCopiedToken] = useState(false); const [copiedToken, setCopiedToken] = useState(false);
const [copiedCurl, setCopiedCurl] = useState(false); const [copiedCurl, setCopiedCurl] = useState(false);
const createNewSite = useCreateSite(); const createNewSite = useCreateSite();
const {
data: gitServers,
isLoading: isLoadingGitServers,
error: gitServersError,
} = useGitServers();
const handleQuickImport = () => { const handleQuickImport = () => {
if (!repoUrl.trim()) return; if (!repoUrl.trim()) return;
const parsed = parseRepoUrl(repoUrl); const parsed = parseRepoUrl(repoUrl);
if (parsed) { if (parsed) {
setGitServer(parsed.gitServer); const gitServerEntry = gitServers?.find((server) =>
server.baseUrl.includes(parsed.gitServer)
);
if (gitServerEntry) {
setGitServer(gitServerEntry.id);
} else {
setUrlError(
'Git server not found. Please make sure the git server is added to the app.'
);
return;
}
setOwner(parsed.owner); setOwner(parsed.owner);
setRepository(parsed.repository); setRepository(parsed.repository);
setUrlError(''); setUrlError('');
@@ -209,15 +209,15 @@ const NewSite = () => {
<div className="space-y-2"> <div className="space-y-2">
<Label>Git Server</Label> <Label>Git Server</Label>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{GIT_SERVERS.map((server) => ( {gitServers?.map((server) => (
<button <button
key={server.value} key={server.id}
type="button" type="button"
onClick={() => setGitServer(server.value)} onClick={() => setGitServer(server.id)}
className={cn( className={cn(
'flex items-center gap-3 rounded-lg border-2 p-4 text-left transition-colors', 'flex items-center gap-3 rounded-lg border-2 p-4 text-left transition-colors',
'hover:bg-accent/15', 'hover:bg-accent/15',
gitServer === server.value gitServer === server.id
? 'border-primary bg-primary/5' ? 'border-primary bg-primary/5'
: 'border-border hover:border-accent/35' : 'border-border hover:border-accent/35'
)} )}
@@ -225,16 +225,44 @@ const NewSite = () => {
<div <div
className={cn( className={cn(
'shrink-0', 'shrink-0',
gitServer === server.value gitServer === server.id
? 'text-primary' ? 'text-primary'
: 'text-muted-foreground' : 'text-muted-foreground'
)} )}
> >
{server.icon} <GitServerTypeIcon type={server.type} />
</div>
<div>
<span className="font-medium">{server.name}</span>
<span className="block text-sm text-muted-foreground">
{server.baseUrl}
</span>
</div> </div>
<span className="font-medium">{server.label}</span>
</button> </button>
))} ))}
{!isLoadingGitServers &&
!gitServersError &&
gitServers?.length === 0 && (
<div className="col-span-2 text-muted-foreground">
No git servers found. Please add a git server first.{' '}
<Link
to="/gitservers"
className="text-primary hover:underline"
>
Add Git Server
</Link>
</div>
)}
{isLoadingGitServers && (
<div className="col-span-2 flex items-center justify-center p-4">
Loading git servers...
</div>
)}
{gitServersError && (
<div className="col-span-2 flex items-center justify-center p-4 text-destructive">
Error loading git servers
</div>
)}
</div> </div>
</div> </div>
+148 -127
View File
@@ -19,138 +19,159 @@ import {
} from '../../components/ui/table'; } from '../../components/ui/table';
import type { Deployment } from '../../api/types/deployments'; import type { Deployment } from '../../api/types/deployments';
import DeploymentStatusBadge from '../../components/DeploymentStatusBadge'; import DeploymentStatusBadge from '../../components/DeploymentStatusBadge';
import { useGitServer } from '../../hooks/api/gitservers/useGitServer';
import type { GitServer } from '../../api/types/gitserver';
const repoUrl = (site: Site) => { const repoUrl = (site: Site, gitServer: GitServer | null) => {
const host = site.git_server === 'github' ? 'github.com' : 'gitlab.com'; if (!gitServer) return 'man';
return `https://${host}/${site.owner}/${site.repository}`; const baseUrl = gitServer.baseUrl.replace(/\/+$/, ''); // Remove trailing slashes
return `${gitServer.protocol}://${baseUrl}/${site.owner}/${site.repository}`;
}; };
const OverviewTab = ({ site, deployments }: { site: Site; deployments?: Deployment[] }) => ( const OverviewTab = ({ site, deployments }: { site: Site; deployments?: Deployment[] }) => {
<div className="grid gap-4 md:grid-cols-2"> const {
<Card> data: gitServer,
<CardHeader> isLoading: isGitServerLoading,
<CardTitle className="text-base">Repository</CardTitle> error: gitServerError,
</CardHeader> } = useGitServer(site.git_server);
<CardContent className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Provider</span>
<span className="capitalize">{site.git_server}</span>
</div>
<Separator />
<div className="flex justify-between">
<span className="text-muted-foreground">Owner / Repo</span>
<a
href={repoUrl(site)}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-primary hover:underline"
>
{site.owner}/{site.repository}
<ExternalLink className="w-3 h-3" />
</a>
</div>
<Separator />
<div className="flex justify-between">
<span className="text-muted-foreground">Branch</span>
<span className="inline-flex items-center gap-1">
<GitBranch className="w-3 h-3" />
{site.branch}
</span>
</div>
</CardContent>
</Card>
<Card> return (
<CardHeader> <div className="grid gap-4 md:grid-cols-2">
<CardTitle className="text-base">Configuration</CardTitle> <Card>
</CardHeader> <CardHeader>
<CardContent className="space-y-3 text-sm"> <CardTitle className="text-base">Repository</CardTitle>
<div className="flex justify-between"> </CardHeader>
<span className="text-muted-foreground">Domain</span> <CardContent className="space-y-3 text-sm">
<a <div className="flex justify-between">
href={`https://${site.domain}`} <span className="text-muted-foreground">Provider</span>
target="_blank" {isGitServerLoading ? (
rel="noopener noreferrer" <span>Loading...</span>
className="inline-flex items-center gap-1 text-primary hover:underline" ) : gitServerError ? (
> <span>Error loading git server</span>
{site.domain} ) : (
<ExternalLink className="w-3 h-3" /> <span>{gitServer?.name}</span>
</a> )}
</div>
<Separator />
<div className="flex justify-between">
<span className="text-muted-foreground">SPA Mode</span>
<div className="inline-flex items-center gap-1">
{site.spa ? <Check className="w-4 h-4" /> : <X className="w-4 h-4" />}
<span>{site.spa ? 'Enabled' : 'Disabled'}</span>
</div> </div>
</div> <Separator />
<Separator /> <div className="flex justify-between">
<div className="flex justify-between"> <span className="text-muted-foreground">Owner / Repo</span>
<span className="text-muted-foreground">Last Deployed</span> <a
<span> href={repoUrl(site, gitServer || null)}
{deployments target="_blank"
? formatDateRelativeOrAbsolute(deployments[0]?.created_at) rel="noopener noreferrer"
: '—'} className="inline-flex items-center gap-1 text-primary hover:underline"
</span> >
</div> {site.owner}/{site.repository}
</CardContent> <ExternalLink className="w-3 h-3" />
</Card> </a>
</div>
<Separator />
<div className="flex justify-between">
<span className="text-muted-foreground">Branch</span>
<span className="inline-flex items-center gap-1">
<GitBranch className="w-3 h-3" />
{site.branch}
</span>
</div>
</CardContent>
</Card>
<Card className="md:col-span-2"> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base">Recent Deployments</CardTitle> <CardTitle className="text-base">Configuration</CardTitle>
<CardDescription>Last 3 deployments for this site.</CardDescription> </CardHeader>
</CardHeader> <CardContent className="space-y-3 text-sm">
<CardContent> <div className="flex justify-between">
{deployments && deployments.length > 0 && ( <span className="text-muted-foreground">Domain</span>
<Table> <a
<TableHeader> href={`https://${site.domain}`}
<TableRow> target="_blank"
<TableHead>Status</TableHead> rel="noopener noreferrer"
<TableHead>Commit</TableHead> className="inline-flex items-center gap-1 text-primary hover:underline"
<TableHead className="hidden sm:table-cell">Message</TableHead> >
<TableHead className="text-right">Duration</TableHead> {site.domain}
<TableHead className="text-right">Time</TableHead> <ExternalLink className="w-3 h-3" />
</TableRow> </a>
</TableHeader> </div>
<TableBody> <Separator />
{deployments.slice(0, 3).map((d) => { <div className="flex justify-between">
const githubCommitUrl = `https://github.com/${site.owner}/${site.repository}/commit/${d.commit_hash}`; <span className="text-muted-foreground">SPA Mode</span>
return ( <div className="inline-flex items-center gap-1">
<TableRow key={d.id}> {site.spa ? <Check className="w-4 h-4" /> : <X className="w-4 h-4" />}
<TableCell> <span>{site.spa ? 'Enabled' : 'Disabled'}</span>
<DeploymentStatusBadge status={d.status} /> </div>
</TableCell> </div>
<TableCell className="font-mono text-xs"> <Separator />
<a <div className="flex justify-between">
href={githubCommitUrl} <span className="text-muted-foreground">Last Deployed</span>
target="_blank" <span>
rel="noopener noreferrer" {deployments
> ? formatDateRelativeOrAbsolute(deployments[0]?.created_at)
{d.commit_hash.substring(0, 7)} : '—'}
</a> </span>
</TableCell> </div>
<TableCell className="hidden sm:table-cell text-muted-foreground"> </CardContent>
{d.message} </Card>
</TableCell>
<TableCell className="text-right text-muted-foreground"> <Card className="md:col-span-2">
{d.start_time && d.finish_time <CardHeader>
? formatDuration(d.start_time, d.finish_time, false) <CardTitle className="text-base">Recent Deployments</CardTitle>
: '—'} <CardDescription>Last 3 deployments for this site.</CardDescription>
</TableCell> </CardHeader>
<TableCell className="text-right text-muted-foreground"> <CardContent>
{formatDateRelativeOrAbsolute(d.created_at)} {deployments && deployments.length > 0 && (
</TableCell> <Table>
</TableRow> <TableHeader>
); <TableRow>
})} <TableHead>Status</TableHead>
</TableBody> <TableHead>Commit</TableHead>
</Table> <TableHead className="hidden sm:table-cell">Message</TableHead>
)} <TableHead className="text-right">Duration</TableHead>
</CardContent> <TableHead className="text-right">Time</TableHead>
</Card> </TableRow>
</div> </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>
);
};
export default OverviewTab; export default OverviewTab;