Skip to content

Commit

Permalink
Upload csv feature (#265)
Browse files Browse the repository at this point in the history
* all changes for upload csv feature

* upddated package.json.lock

* updated package.lock.json

* update package lock

* fixes

* validation

---------

Co-authored-by: Oluwadabira Akinwumi <[email protected]>
Co-authored-by: Karthik Kalyanaraman <[email protected]>
  • Loading branch information
3 people authored Oct 16, 2024
1 parent 0cddf71 commit 67dd8d8
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 17,984 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { PathBreadCrumbs } from "@/components/dataset/path-breadcrumbs";
import { ExpandingTextArea } from "@/components/playground/common";
import { UploadCsv } from "@/components/project/dataset/upload-csv";
import { CreateData } from "@/components/project/dataset/create-data";
import DatasetRowSkeleton from "@/components/project/dataset/dataset-row-skeleton";
import { DeleteData } from "@/components/project/dataset/delete-data";
Expand Down Expand Up @@ -394,6 +395,7 @@ export default function Dataset() {
<div className="flex justify-between items-center">
<div className="flex gap-4 items-center w-fit">
<CreateData projectId={projectId} datasetId={dataset_id} />
<UploadCsv projectId={projectId} datasetId={dataset_id}/>
<DownloadDataset
projectId={projectId}
datasetId={dataset_id}
Expand Down
5 changes: 2 additions & 3 deletions app/api/project/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,9 @@ export async function POST(req: NextRequest) {
});
}

const session = await getServerSession(authOptions);
const userEmail = session?.user?.email ?? "anonymous";
await captureEvent(project.id, "project_created", {
project_name: project.name,
project_name: name,
project_description: description,
project_type: projectType,
});
}
Expand Down
301 changes: 301 additions & 0 deletions components/project/dataset/upload-csv.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
import { Info } from "@/components/shared/info";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon, UploadIcon } from "@radix-ui/react-icons";
import { Check } from "lucide-react";
import papa from "papaparse";
import React, { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { useQueryClient } from "react-query";
import { toast } from "sonner";
import { z } from "zod";

export function UploadCsv({
datasetId,
projectId,
disabled = false,
className = "",
}: {
datasetId?: string;
projectId?: string;
disabled?: boolean;
className?: string;
}) {
const csvFileRef = useRef<HTMLInputElement>(null);
const queryClient = useQueryClient();
const [open, setOpen] = useState<boolean>(false);
const [busy, setBusy] = useState<boolean>(false);
const [csvFields, setCsvFields] = useState<string[]>([]);
const [myData, setMyData] = useState<any[]>([]);
const [selectedInputHeader, setSelectedInputHeader] = useState<string>("");
const [selectedOutputHeader, setSelectedOutputHeader] = useState<string>("");
const [selectedNoteHeader, setSelectedNoteHeader] = useState<string>("");
const MAX_UPLOAD_SIZE = 1024 * 1024 * 0.5; // 0.5MB = 512KB
const ACCEPTED_FILE_TYPES = ["text/csv"];
const schema = z.object({
file: z
.custom<File>((val) => val instanceof File, "Please upload a file")
.refine((file) => ACCEPTED_FILE_TYPES.includes(file.type), {
message: "Please select csv files only",
})
.refine((file) => file.size <= MAX_UPLOAD_SIZE, {
message: "Your file is too large, file has to be a max of 512KB",
}),
});

const CreateDataForm = useForm({
resolver: zodResolver(schema),
});

const parseData = (files: FileList) => {
let file: any = files[0];
papa.parse(file, {
skipEmptyLines: true,
header: true,
complete: (result: any) => {
const papa_data: any[] = result.data.map((row: any) => {
return row;
});
setMyData(papa_data ?? []);
const newFields: string[] = result.meta.fields?.map((field: any) => {
return field;
})!;
setCsvFields(newFields ?? []);
},
});
};

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button disabled={disabled} variant={"outline"} className={className}>
Upload csv <UploadIcon className="ml-2" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Create Data</DialogTitle>
<DialogDescription>
Upload a csv file to create data for the dataset.
</DialogDescription>
</DialogHeader>
<Form {...CreateDataForm}>
<form
onSubmit={CreateDataForm.handleSubmit(async (data) => {
try {
setBusy(true);
await fetch("/api/data", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
datas: myData,
datasetId,
projectId,
}),
});
await queryClient.invalidateQueries(datasetId);
toast("Data added!", {
description: "Your data has been added.",
});
setOpen(false);
CreateDataForm.reset();
} catch (error: any) {
toast("Error added your data!", {
description: `There was an error added your data: ${error.message}`,
});
} finally {
setBusy(false);
}
})}
className="flex flex-col gap-4"
>
<FormField
disabled={busy}
control={CreateDataForm.control}
name="file"
render={({ field }) => (
<FormItem>
<FormLabel>
Select a CSV File
<Info
information="Upload a CSV file to create data for the dataset. The CSV file should have the an 'input', 'output' and 'note' columns."
className="inline-block ml-2"
/>
</FormLabel>
<FormControl>
<Input
type="file"
onChange={(event) => {
field.onChange(event.target?.files?.[0] ?? undefined);
event.target.files!.length > 0 &&
event.target.files![0].type == "text/csv"
? parseData(csvFileRef.current?.files!)
: setCsvFields([]);
}}
ref={csvFileRef}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Label>Input</Label>
<Info
information="The input column is the column that contains the input data."
className="inline-block ml-2"
/>
</div>
<div className="flex flex-col gap-2">
<HeaderSelect
headers={csvFields}
setSelectedHeader={setSelectedInputHeader}
selectedHeader={selectedInputHeader}
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<Label>Output</Label>
<Info
information="The output column is the column that contains the output data."
className="inline-block ml-2"
/>
</div>
<HeaderSelect
headers={csvFields}
setSelectedHeader={setSelectedOutputHeader}
selectedHeader={selectedOutputHeader}
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<Label>Note</Label>
<Info
information="The note column is the column that contains the note data."
className="inline-block ml-2"
/>
</div>
<HeaderSelect
headers={csvFields}
setSelectedHeader={setSelectedNoteHeader}
selectedHeader={selectedNoteHeader}
/>
</div>
</div>
<DialogFooter>
<Button
type="submit"
disabled={
busy ||
!selectedInputHeader ||
!selectedOutputHeader ||
!selectedNoteHeader
}
>
Create Data
<PlusIcon className="h-4 w-4 ml-2" />
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}

export default function HeaderSelect({
headers,
setSelectedHeader,
selectedHeader,
}: {
headers: string[];
setSelectedHeader: (header: string) => void;
selectedHeader: string;
}) {
const [open, setOpen] = React.useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full z-[920] justify-between"
>
{selectedHeader
? headers.length > 0
? headers.find((header: string) => header === selectedHeader)
: "No headers found"
: "Select header..."}
</Button>
</PopoverTrigger>
<PopoverContent className="z-[920] w-[370px] p-0">
<Command>
<CommandInput placeholder="Search header..." />
<CommandEmpty>No header found.</CommandEmpty>
<CommandGroup>
{headers.length > 0 ? (
headers.map((header: string, index: number) => (
<CommandItem
key={index}
value={header}
onSelect={(currentValue) => {
setSelectedHeader(
currentValue === selectedHeader ? "" : currentValue
);
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedHeader === header ? "opacity-100" : "opacity-0"
)}
/>
{header}
</CommandItem>
))
) : (
<CommandItem>No headers found</CommandItem>
)}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
}
Loading

0 comments on commit 67dd8d8

Please sign in to comment.