mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
feat(web): Support GitLab repository urls.
Add GitLab URL input mode alongside existing GitHub and local modes: - GitLab URL validation for gitlab.com and self-hosted instances - GitLab icon component (custom SVG, matching existing GitHub icon pattern) - Mode tab UI with GitLab option - Backend API integration for GitLab HTTPS URLs No token configuration included — public repositories supported only. Close: #378 Co-authored-by: Claude <noreply@anthropic.com> AI-model: kimi-for-coding/k2p6
This commit is contained in:
parent
c30833fad3
commit
690163a865
2 changed files with 157 additions and 4 deletions
|
|
@ -9,6 +9,7 @@
|
|||
import { useState, useRef, useEffect, useId } from 'react';
|
||||
import {
|
||||
Github,
|
||||
Gitlab,
|
||||
FolderOpen,
|
||||
Loader2,
|
||||
Check,
|
||||
|
|
@ -26,15 +27,20 @@ import { AnalyzeProgress } from './AnalyzeProgress';
|
|||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
type InputMode = 'github' | 'local';
|
||||
type InputMode = 'github' | 'gitlab' | 'local';
|
||||
|
||||
const GITHUB_RE = /^https?:\/\/(www\.)?github\.com\/[^/\s]+\/[^/\s]+/i;
|
||||
const GITLAB_RE = /^https?:\/\/[^/\s]+\/[^/\s]+\/[^/\s]+(\/.*)?$/i;
|
||||
const IS_WINDOWS = navigator.userAgent.toLowerCase().includes('win');
|
||||
|
||||
function isValidGithubUrl(value: string): boolean {
|
||||
return GITHUB_RE.test(value.trim());
|
||||
}
|
||||
|
||||
function isValidGitlabUrl(value: string): boolean {
|
||||
return GITLAB_RE.test(value.trim());
|
||||
}
|
||||
|
||||
// ── Mode tabs ────────────────────────────────────────────────────────────────
|
||||
|
||||
function ModeTabs({ mode, onChange }: { mode: InputMode; onChange: (m: InputMode) => void }) {
|
||||
|
|
@ -53,6 +59,19 @@ function ModeTabs({ mode, onChange }: { mode: InputMode; onChange: (m: InputMode
|
|||
<Github className="h-3 w-3" />
|
||||
GitHub URL
|
||||
</button>
|
||||
<button
|
||||
role="tab"
|
||||
aria-selected={mode === 'gitlab'}
|
||||
onClick={() => onChange('gitlab')}
|
||||
className={`flex flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-all duration-150 ${
|
||||
mode === 'gitlab'
|
||||
? 'bg-accent text-white shadow-sm'
|
||||
: 'text-text-muted hover:text-text-secondary'
|
||||
} `}
|
||||
>
|
||||
<Gitlab className="h-3 w-3" />
|
||||
GitLab URL
|
||||
</button>
|
||||
<button
|
||||
role="tab"
|
||||
aria-selected={mode === 'local'}
|
||||
|
|
@ -138,6 +157,7 @@ export const RepoAnalyzer = ({ variant, onComplete, onCancel }: RepoAnalyzerProp
|
|||
const folderInputRef = useRef<HTMLInputElement>(null);
|
||||
const [mode, setMode] = useState<InputMode>('github');
|
||||
const [githubUrl, setGithubUrl] = useState('');
|
||||
const [gitlabUrl, setGitlabUrl] = useState('');
|
||||
const [localPath, setLocalPath] = useState('');
|
||||
const [phase, setPhase] = useState<InternalPhase>('input');
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
|
|
@ -162,6 +182,7 @@ export const RepoAnalyzer = ({ variant, onComplete, onCancel }: RepoAnalyzerProp
|
|||
const handleModeChange = (m: InputMode) => {
|
||||
setMode(m);
|
||||
setGithubUrl('');
|
||||
setGitlabUrl('');
|
||||
setLocalPath('');
|
||||
setValidationError(null);
|
||||
};
|
||||
|
|
@ -175,13 +196,19 @@ export const RepoAnalyzer = ({ variant, onComplete, onCancel }: RepoAnalyzerProp
|
|||
const canSubmit =
|
||||
mode === 'github'
|
||||
? isValidGithubUrl(githubUrl) && (phase === 'input' || phase === 'error')
|
||||
: localPath.trim().length > 1 && (phase === 'input' || phase === 'error');
|
||||
: mode === 'gitlab'
|
||||
? isValidGitlabUrl(gitlabUrl) && (phase === 'input' || phase === 'error')
|
||||
: localPath.trim().length > 1 && (phase === 'input' || phase === 'error');
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
if (mode === 'github' && !isValidGithubUrl(githubUrl)) {
|
||||
setValidationError('Please enter a valid GitHub repository URL.');
|
||||
return;
|
||||
}
|
||||
if (mode === 'gitlab' && !isValidGitlabUrl(gitlabUrl)) {
|
||||
setValidationError('Please enter a valid GitLab repository URL.');
|
||||
return;
|
||||
}
|
||||
if (mode === 'local' && localPath.trim().length < 2) {
|
||||
setValidationError('Please enter a folder path.');
|
||||
return;
|
||||
|
|
@ -191,12 +218,22 @@ export const RepoAnalyzer = ({ variant, onComplete, onCancel }: RepoAnalyzerProp
|
|||
setPhase('starting');
|
||||
|
||||
try {
|
||||
const request = mode === 'github' ? { url: githubUrl.trim() } : { path: localPath.trim() };
|
||||
const request =
|
||||
mode === 'github'
|
||||
? { url: githubUrl.trim() }
|
||||
: mode === 'gitlab'
|
||||
? { url: gitlabUrl.trim() }
|
||||
: { path: localPath.trim() };
|
||||
const { jobId } = await startAnalyze(request);
|
||||
jobIdRef.current = jobId;
|
||||
setPhase('analyzing');
|
||||
|
||||
const nameSource = mode === 'github' ? githubUrl.trim() : localPath.trim();
|
||||
const nameSource =
|
||||
mode === 'github'
|
||||
? githubUrl.trim()
|
||||
: mode === 'gitlab'
|
||||
? gitlabUrl.trim()
|
||||
: localPath.trim();
|
||||
const controller = streamAnalyzeProgress(
|
||||
jobId,
|
||||
(p) => setProgress(p),
|
||||
|
|
@ -297,6 +334,61 @@ export const RepoAnalyzer = ({ variant, onComplete, onCancel }: RepoAnalyzerProp
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* GitLab URL input */}
|
||||
{showInput && mode === 'gitlab' && (
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="block text-xs font-medium tracking-wider text-text-secondary uppercase"
|
||||
>
|
||||
GitLab Repository URL
|
||||
</label>
|
||||
<div
|
||||
className={`flex items-center gap-3 rounded-xl border bg-void px-4 py-3.5 transition-all duration-200 ${
|
||||
validationError && phase === 'error'
|
||||
? 'border-red-500/50'
|
||||
: isValidGitlabUrl(gitlabUrl)
|
||||
? 'border-accent/50 shadow-[0_0_0_3px_rgba(124,58,237,0.08)]'
|
||||
: 'border-border-default focus-within:border-accent/40'
|
||||
} `}
|
||||
>
|
||||
<Gitlab className="h-4 w-4 shrink-0 text-text-muted" />
|
||||
<input
|
||||
id={inputId}
|
||||
type="url"
|
||||
value={gitlabUrl}
|
||||
onChange={(e) => {
|
||||
setGitlabUrl(e.target.value);
|
||||
if (validationError) setValidationError(null);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && canSubmit && !isLoading) {
|
||||
e.preventDefault();
|
||||
handleAnalyze();
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
placeholder="https://gitlab.com/owner/repo"
|
||||
autoComplete="url"
|
||||
spellCheck={false}
|
||||
className="flex-1 border-none bg-transparent font-mono text-sm text-text-primary outline-none placeholder:text-text-muted disabled:opacity-50"
|
||||
/>
|
||||
{gitlabUrl.length > 10 && (
|
||||
<div className="shrink-0">
|
||||
{isValidGitlabUrl(gitlabUrl) ? (
|
||||
<Check className="h-3.5 w-3.5 text-emerald-400" />
|
||||
) : (
|
||||
<AlertCircle className="h-3.5 w-3.5 text-text-muted" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-text-muted">
|
||||
Supports GitLab.com and self-hosted GitLab instances.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Local folder input */}
|
||||
{showInput && mode === 'local' && (
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -123,6 +123,67 @@ export {
|
|||
* defaults to `currentColor`, so Tailwind `text-*` utilities work the same as
|
||||
* with any other icon in this module.
|
||||
*/
|
||||
/**
|
||||
* GitLab tanuki mark — SVG path data from simple-icons (CC0-1.0).
|
||||
*
|
||||
* GitLab's logo (the tanuki/fox-head) is a registered trademark of GitLab Inc.
|
||||
* We use it here only to indicate GitLab source-repo integration.
|
||||
*
|
||||
* API-compatible with `lucide-react` icons (`LucideProps`).
|
||||
*/
|
||||
export const Gitlab = forwardRef<SVGSVGElement, LucideProps>(function Gitlab(
|
||||
{
|
||||
size = 24,
|
||||
color = 'currentColor',
|
||||
className,
|
||||
strokeWidth: _strokeWidth,
|
||||
absoluteStrokeWidth: _absoluteStrokeWidth,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const numericSize = typeof size === 'string' ? Number.parseFloat(size) : size;
|
||||
const useSmallVariant = Number.isFinite(numericSize) && (numericSize as number) <= 16;
|
||||
|
||||
if (useSmallVariant) {
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 16 16"
|
||||
fill={color}
|
||||
className={className}
|
||||
{...rest}
|
||||
>
|
||||
<path d="M8 15.282l1.855-5.717H6.145L8 15.282z" />
|
||||
<path d="M8 15.282L6.145 9.565H2.333L8 15.282z" />
|
||||
<path d="M2.333 9.565l-.944-2.942c-.09-.267.067-.553.333-.553h3.153L2.333 9.565z" />
|
||||
<path d="M4.875 6.07L6.145 9.565H2.333l2.542-3.495z" />
|
||||
<path d="M13.667 9.565l.944-2.942c.09-.267-.067-.553-.333-.553h-3.153l2.542 3.495z" />
|
||||
<path d="M11.125 6.07L9.855 9.565h3.812l-2.542-3.495z" />
|
||||
<path d="M8 15.282l1.855-5.717H6.145L8 15.282z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill={color}
|
||||
className={className}
|
||||
{...rest}
|
||||
>
|
||||
<path d="m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z" />
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
|
||||
export const Github = forwardRef<SVGSVGElement, LucideProps>(function Github(
|
||||
{
|
||||
size = 24,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue