Add select for git servers when creating site

This commit is contained in:
2026-05-03 19:36:24 +02:00
parent a68368df3c
commit c3f4c95150
6 changed files with 253 additions and 187 deletions
@@ -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) {
const match = cleaned.match(pattern);
if (match) {
const host = match[1];
const server = host === 'github.com' ? 'github' : 'gitlab';
return { gitServer: server, owner: match[2], repository: match[3] };
} }
// SSH: git@host:owner/repo
const sshMatch = cleaned.match(/^git@([^:]+):([^/]+)\/([^/]+)/);
if (sshMatch) {
return { gitServer: sshMatch[1], owner: sshMatch[2], repository: sshMatch[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>
@@ -19,13 +19,23 @@ 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[] }) => {
const {
data: gitServer,
isLoading: isGitServerLoading,
error: gitServerError,
} = useGitServer(site.git_server);
return (
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<Card> <Card>
<CardHeader> <CardHeader>
@@ -34,13 +44,19 @@ const OverviewTab = ({ site, deployments }: { site: Site; deployments?: Deployme
<CardContent className="space-y-3 text-sm"> <CardContent className="space-y-3 text-sm">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted-foreground">Provider</span> <span className="text-muted-foreground">Provider</span>
<span className="capitalize">{site.git_server}</span> {isGitServerLoading ? (
<span>Loading...</span>
) : gitServerError ? (
<span>Error loading git server</span>
) : (
<span>{gitServer?.name}</span>
)}
</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">Owner / Repo</span>
<a <a
href={repoUrl(site)} href={repoUrl(site, gitServer || null)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-primary hover:underline" className="inline-flex items-center gap-1 text-primary hover:underline"
@@ -136,7 +152,11 @@ const OverviewTab = ({ site, deployments }: { site: Site; deployments?: Deployme
</TableCell> </TableCell>
<TableCell className="text-right text-muted-foreground"> <TableCell className="text-right text-muted-foreground">
{d.start_time && d.finish_time {d.start_time && d.finish_time
? formatDuration(d.start_time, d.finish_time, false) ? formatDuration(
d.start_time,
d.finish_time,
false
)
: '—'} : '—'}
</TableCell> </TableCell>
<TableCell className="text-right text-muted-foreground"> <TableCell className="text-right text-muted-foreground">
@@ -151,6 +171,7 @@ const OverviewTab = ({ site, deployments }: { site: Site; deployments?: Deployme
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
); );
};
export default OverviewTab; export default OverviewTab;