Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
862 changes: 523 additions & 339 deletions client/src/components/DatabaseConfigForm.tsx

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions client/src/components/DbSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { Badge } from "@/components/ui/badge";
import { Database, Server, Layers, ArrowRight, Box } from "lucide-react";
import { cn } from "@/lib/utils";
import { motion, Variants } from "framer-motion";
import { Database, Server, Layers, ArrowRight, Box, FileArchiveIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import { motion, Variants } from 'framer-motion';

interface DbSelectorProps {
onSelect: (db: string) => void;
selected?: string;
onSelect: (db: string) => void;
selected?: string;
}

const DATABASES = [
{ id: "mongodb", name: "MongoDB", status: "available", icon: Database },
{ id: "postgres", name: "PostgreSQL", status: "available", icon: Server },
{ id: "mysql", name: "MySQL", status: "available", icon: Layers },
{ id: "redis", name: "Redis", status: "available", icon: Box },
{ id: 'mongodb', name: 'MongoDB', status: 'available', icon: Database },
{ id: 'postgres', name: 'PostgreSQL', status: 'available', icon: Server },
{ id: 'mysql', name: 'MySQL', status: 'available', icon: Layers },
{ id: 'redis', name: 'Redis', status: 'available', icon: Box },
{ id: 'firebase', name: 'Firebase', status: 'available', icon: FileArchiveIcon },
];

const container: Variants = {
Expand Down
48 changes: 48 additions & 0 deletions client/src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { SetStateAction } from 'react';

type FirebaseMode = 'rtdb' | 'firestore';
``;
interface CheckBoxProps {
options: { label: string; value: string }[];
firebaseMode: string;
onClick: (value: SetStateAction<FirebaseMode>) => void;
}

const CheckBox = ({ options, firebaseMode, onClick }: CheckBoxProps) => {
return (
<div className='grid grid-cols-2 gap-3'>
{options.map((item) => {
const active = firebaseMode === item.value;
return (
<button
key={item.value}
type='button'
onClick={() => onClick(item.value as FirebaseMode)}
className={`relative p-4 rounded-xl border transition-all text-left
${
active
? 'bg-indigo-600/20 border-indigo-500 shadow-lg shadow-indigo-500/10'
: 'bg-white/[0.03] border-white/10 hover:border-white/20'
}
`}
>
<div className='flex items-center justify-between'>
<span className='text-sm font-medium text-white'>{item.label}</span>

{/* indicator */}
<div
className={`h-4 w-4 rounded-full border flex items-center justify-center
${active ? 'border-indigo-400' : 'border-white/30'}
`}
>
{active && <div className='h-2 w-2 rounded-full bg-indigo-400' />}
</div>
</div>
</button>
);
})}
</div>
);
};

export { CheckBox };
55 changes: 55 additions & 0 deletions client/src/components/ui/uploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ChangeEvent } from 'react';
import { Label } from './label';

interface UploaderProps {
id: string;
handleFirebaseFile: (e: ChangeEvent<HTMLInputElement>, type: string) => Promise<void>;
firebaseError: string | null;
firebaseConfig: { projectId: string } | null;
}

const Uploader = ({ id, handleFirebaseFile, firebaseError, firebaseConfig }: UploaderProps) => (
<div className='space-y-3'>
<Label className='text-xs font-bold uppercase tracking-wider text-muted-foreground/70'>
Firebase Service Account
</Label>

<label htmlFor={id} className='block cursor-pointer'>
<input
id={id}
type='file'
accept='application/json'
onChange={(e) => handleFirebaseFile(e, id)}
className='hidden'
/>

<div
className={`p-5 rounded-xl border border-dashed transition-all text-center
${
firebaseError
? 'border-red-500/40 bg-red-500/5'
: firebaseConfig
? 'border-green-500/40 bg-green-500/5'
: 'border-white/10 bg-white/[0.03] hover:border-white/20'
}
`}
>
<div className='space-y-2'>
<p className='text-sm text-white font-medium'>
{firebaseConfig ? 'Service account loaded' : 'Click to upload JSON'}
</p>

<p className='text-xs text-muted-foreground'>
{firebaseConfig
? firebaseConfig.projectId
: 'Only Firebase service account files'}
</p>

{firebaseError && <p className='text-xs text-red-400'>{firebaseError}</p>}
</div>
</div>
</label>
</div>
);

export { Uploader };
125 changes: 70 additions & 55 deletions client/src/pages/ConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,69 +9,84 @@ export function ConfigPage() {
const navigate = useNavigate();

const handleStartCopy = async (config: {
sourceUri: string;
targetUri: string;
sourceUri: string;
targetUri: string;
targetCredent?: string;
sourceCredent?: string;
firebaseType?: string;
}) => {
try {
const res = await api.post("/migrate/start", {
type: "copy",
sourceUri: config.sourceUri,
targetUri: config.targetUri,
dbType: dbType,
});
try {
const res = await api.post('/migrate/start', {
type: 'copy',
sourceUri: config.sourceUri,
targetUri: config.targetUri,
firebaseType: config.firebaseType,
sourceCredent: config.sourceCredent,
targetCredent: config.targetCredent,
dbType: dbType,
});

const { jobId } = res.data;
const { jobId } = res.data;

// Store migration config for retry functionality
sessionStorage.setItem(
`migration_${jobId}`,
JSON.stringify({
type: "copy",
sourceUri: config.sourceUri,
targetUri: config.targetUri,
dbType: dbType,
}),
);
// Store migration config for retry functionality
localStorage.setItem(
`migration_${jobId}`,
JSON.stringify({
type: 'copy',
sourceUri: config.sourceUri,
targetUri: config.targetUri,
sourceCredent: config.sourceCredent,
targetCredent: config.targetCredent,
firebaseType: config.firebaseType,
dbType: dbType,
}),
);

toast.success("Migration started", {
description: "Your data is being transferred securely.",
});
navigate(`/migration/${jobId}`);
} catch (err: any) {
console.error(err);
const msg = err.response?.data?.error || "Failed to start migration job.";
toast.error("Migration failed", { description: msg });
throw err; // Re-throw to let form handle loading state
}
toast.success('Migration started', {
description: 'Your data is being transferred securely.',
});
navigate(`/migration/${jobId}`);
} catch (err: any) {
console.error(err);
const msg = err.response?.data?.error || 'Failed to start migration job.';
toast.error('Migration failed', { description: msg });
throw err; // Re-throw to let form handle loading state
}
};

const handleStartDownload = async (config: { sourceUri: string }) => {
const promise = async () => {
const response = await api.post(
"/download",
{
sourceUri: config.sourceUri,
dbType: dbType,
},
{
responseType: "blob",
},
);
const handleStartDownload = async (config: {
sourceUri: string;
credent?: any;
type?: string;
}) => {
const promise = async () => {
const response = await api.post(
'/download',
{
sourceUri: config.sourceUri,
credent: config.credent,
type: config.type,
dbType: dbType,
},
{
responseType: 'blob',
},
);

const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", `dump_${Date.now()}.zip`);
document.body.appendChild(link);
link.click();
link.remove();
};
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `dump_${Date.now()}.zip`);
document.body.appendChild(link);
link.click();
link.remove();
};

toast.promise(promise(), {
loading: "Preparing download...",
success: "Download started!",
error: "Export failed.",
});
toast.promise(promise(), {
loading: 'Preparing download...',
success: 'Download started!',
error: 'Export failed.',
});
};

return (
Expand Down
Loading