feat: new importer (#214)
This commit is contained in:
committed by
GitHub
parent
b51bc8f3f6
commit
212254d31a
@@ -0,0 +1,303 @@
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import {
|
||||
IntegrationCard,
|
||||
IntegrationCardFooter,
|
||||
IntegrationCardLogoImage,
|
||||
} from '@/components/integrations/integration-card';
|
||||
import { Skeleton } from '@/components/skeleton';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Tooltiper } from '@/components/ui/tooltip';
|
||||
import { useAppParams } from '@/hooks/use-app-params';
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { pushModal } from '@/modals';
|
||||
import { IMPORT_PROVIDERS } from '@openpanel/importer/providers';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
Download,
|
||||
InfoIcon,
|
||||
Loader2Icon,
|
||||
XCircleIcon,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const Route = createFileRoute(
|
||||
'/_app/$organizationId/$projectId_/settings/_tabs/imports',
|
||||
)({
|
||||
component: ImportsSettings,
|
||||
});
|
||||
|
||||
function ImportsSettings() {
|
||||
const { projectId } = useAppParams();
|
||||
const trpc = useTRPC();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const importsQuery = useQuery(
|
||||
trpc.import.list.queryOptions(
|
||||
{ projectId },
|
||||
{
|
||||
refetchInterval: 5000,
|
||||
},
|
||||
),
|
||||
);
|
||||
const imports = importsQuery.data ?? [];
|
||||
|
||||
const deleteImport = useMutation(
|
||||
trpc.import.delete.mutationOptions({
|
||||
onSuccess: () => {
|
||||
toast.success('Import deleted', {
|
||||
description: 'The import has been successfully deleted.',
|
||||
});
|
||||
queryClient.invalidateQueries(trpc.import.list.pathFilter());
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const retryImport = useMutation(
|
||||
trpc.import.retry.mutationOptions({
|
||||
onSuccess: () => {
|
||||
toast.success('Import retried', {
|
||||
description: 'The import has been queued for processing again.',
|
||||
});
|
||||
queryClient.invalidateQueries(trpc.import.list.pathFilter());
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const handleProviderSelect = (
|
||||
provider: (typeof IMPORT_PROVIDERS)[number],
|
||||
) => {
|
||||
pushModal('AddImport', {
|
||||
provider: provider.id,
|
||||
name: provider.name,
|
||||
types: provider.types,
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string, errorMessage: string | null) => {
|
||||
const variants: Record<string, any> = {
|
||||
pending: 'secondary',
|
||||
processing: 'default',
|
||||
completed: 'success',
|
||||
failed: 'destructive',
|
||||
};
|
||||
const icons: Record<string, React.ReactNode> = {
|
||||
pending: <Loader2Icon className="w-4 h-4 animate-spin" />,
|
||||
processing: <Loader2Icon className="w-4 h-4 animate-spin" />,
|
||||
completed: <CheckCircleIcon className="w-4 h-4" />,
|
||||
failed: <XCircleIcon className="w-4 h-4" />,
|
||||
};
|
||||
|
||||
if (status === 'failed') {
|
||||
return (
|
||||
<Tooltiper
|
||||
content={errorMessage}
|
||||
tooltipClassName="max-w-xs break-words"
|
||||
>
|
||||
<Badge variant={variants[status] || 'default'} className="capitalize">
|
||||
{icons[status] || null}
|
||||
{status}
|
||||
</Badge>
|
||||
</Tooltiper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge variant={variants[status] || 'default'} className="capitalize">
|
||||
{icons[status] || null}
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{IMPORT_PROVIDERS.map((provider) => (
|
||||
<IntegrationCard
|
||||
key={provider.id}
|
||||
icon={
|
||||
<IntegrationCardLogoImage
|
||||
src={provider.logo}
|
||||
backgroundColor={provider.backgroundColor}
|
||||
className="p-4"
|
||||
/>
|
||||
}
|
||||
name={provider.name}
|
||||
description={provider.description}
|
||||
>
|
||||
<IntegrationCardFooter className="row justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => handleProviderSelect(provider)}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Import Data
|
||||
</Button>
|
||||
</IntegrationCardFooter>
|
||||
</IntegrationCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-medium mb-4">Import History</h3>
|
||||
|
||||
<div className="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Provider</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Progress</TableHead>
|
||||
<TableHead>Config</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{!importsQuery.isLoading && imports.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6}>
|
||||
<FullPageEmptyState
|
||||
title="No imports yet"
|
||||
description="Your import history will appear here."
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{importsQuery.isLoading &&
|
||||
[1, 2, 3, 4].map((index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
</TableCell>
|
||||
<TableCell className="text-right justify-end row">
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{imports.map((imp) => (
|
||||
<TableRow key={imp.id}>
|
||||
<TableCell className="font-medium capitalize">
|
||||
<div className="row gap-2 items-center">
|
||||
<div>{imp.config.provider}</div>
|
||||
<Badge variant="outline" className="uppercase">
|
||||
{imp.config.type}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDistanceToNow(new Date(imp.createdAt), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
{getStatusBadge(imp.status, imp.errorMessage)}
|
||||
{imp.statusMessage && (
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
{imp.statusMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{imp.totalEvents > 0 ? (
|
||||
<div className="space-y-1 font-mono">
|
||||
<div className="text-sm">
|
||||
{imp.processedEvents.toLocaleString()}
|
||||
{' / '}
|
||||
<Tooltiper
|
||||
content="Estimated number of events. Can be inaccurate depending on the provider."
|
||||
tooltipClassName="max-w-xs"
|
||||
>
|
||||
{imp.totalEvents.toLocaleString()}{' '}
|
||||
<InfoIcon className="w-4 h-4 inline-block relative -top-px" />
|
||||
</Tooltiper>
|
||||
</div>
|
||||
{imp.status === 'processing' && (
|
||||
<div className="w-full bg-secondary rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-primary h-1.5 rounded-full transition-all"
|
||||
style={{
|
||||
width: `${Math.min(Math.round((imp.processedEvents / imp.totalEvents) * 100), 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : imp.totalEvents === -1 ? (
|
||||
<div className="font-mono text-sm">
|
||||
{imp.processedEvents.toLocaleString()}
|
||||
{' / '}
|
||||
N/A
|
||||
</div>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Tooltiper
|
||||
content={
|
||||
<pre className="font-mono text-sm leading-normal whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(imp.config, null, 2)}
|
||||
</pre>
|
||||
}
|
||||
tooltipClassName="max-w-xs"
|
||||
>
|
||||
<Badge>Config</Badge>
|
||||
</Tooltiper>
|
||||
</TableCell>
|
||||
<TableCell className="text-right space-x-2">
|
||||
{imp.status === 'failed' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => retryImport.mutate({ id: imp.id })}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => deleteImport.mutate({ id: imp.id })}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -42,6 +42,7 @@ function ProjectDashboard() {
|
||||
{ id: 'details', label: 'Details' },
|
||||
{ id: 'events', label: 'Events' },
|
||||
{ id: 'clients', label: 'Clients' },
|
||||
{ id: 'imports', label: 'Imports' },
|
||||
];
|
||||
|
||||
const handleTabChange = (tabId: string) => {
|
||||
|
||||
Reference in New Issue
Block a user