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
36 changes: 24 additions & 12 deletions backend/app/api/endpoints/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,18 +130,30 @@ async def get_dataset_details(dataset_id: int, db: Session = Depends(database.ge
return data


@router.get("/recent", response_model=List[schemas.LastResponse])
def get_recent_datasets(db: Session = Depends(database.get_db)):
recent_datasets = db.query(models.Dataset).order_by(models.Dataset.last_modified.desc()).limit(3).all()
return [
schemas.LastResponse(
dataset_id=dataset.dataset_id,
name=dataset.name,
description=dataset.description,
last_modified=dataset.last_modified
) for dataset in recent_datasets
]

@router.get("/recent",response_model=schemas.PaginatedResponse[schemas.LastResponse])
def get_recent_datasets(
page: int = Query(1, alias="page", ge=1),
limit: int = Query(10, alias="limit", le=100),
db: Session = Depends(database.get_db)
):
skip = (page - 1) * limit
datasets = db.query(models.Dataset).order_by(models.Dataset.last_modified.desc()).offset(skip).limit(limit).all()
total = db.query(models.Dataset).count()

return schemas.PaginatedResponse[schemas.LastResponse](
data=[
schemas.LastResponse(
dataset_id=dataset.dataset_id,
name=dataset.name,
description=dataset.description,
last_modified=dataset.last_modified
) for dataset in datasets
],
total=total,
page=page,
limit=limit,
has_more=(page * limit) < total
)

@router.post("/{dataset_id}/transform", response_model=schemas.BasicQueryResponse)
async def transform_dataset(
Expand Down
11 changes: 9 additions & 2 deletions backend/app/schemas.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from pydantic import BaseModel
from pydantic import BaseModel
from enum import Enum
from typing import Optional, Union, Any, List
from typing import Optional, TypeVar, Union, Any, List , Generic
import datetime

T = TypeVar("T")

# Basic Functions
class FilterParameters(BaseModel):
Expand Down Expand Up @@ -130,4 +131,10 @@ class LastResponse(BaseModel):
class Config:
from_attributes = True

class PaginatedResponse(BaseModel, Generic[T]):
data: List[T]
total: int
page: int
limit: int
has_more: bool

20 changes: 15 additions & 5 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -10,25 +10,35 @@
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^4.0.0",
"@repo/ui": "*",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.7.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.475.0",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.54.2",
"react-icons": "^5.2.1",
"react-router-dom": "^6.24.0"
"react-router-dom": "^6.24.0",
"sonner": "^1.7.4",
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.4",
"postcss": "^8.5.2",
"tailwindcss": "^3.4.17",
"vite": "^5.2.0"
}
}
5 changes: 5 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import {BrowserRouter as Router, Route, Routes, useLocation} from 'react-router-
import DataScreen from './Components/DataScreen';
import HomeScreen from './Components/Homescreen';
import Navbar from './Components/Navbar';
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

function App() {
return (
<Router>
<QueryClientProvider client={queryClient}>
<AppContent />
</QueryClientProvider>
</Router>
);
}
Expand Down
148 changes: 148 additions & 0 deletions frontend/src/Components/CreateProjectModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { uploadDataset } from '../api';

const CreateProjectModal = ({ isOpen, onClose }) => {
const [fileUpload, setFileUpload] = useState(null);
const [formData, setFormData] = useState({
projectName: "",
projectDescription: ""
});
const [errors, setErrors] = useState({});
const navigate = useNavigate();
const queryClient = useQueryClient();

// React Query mutation for uploading dataset
const uploadMutation = useMutation({
mutationFn: async ({ file, projectName, projectDescription }) => {
return await uploadDataset(file, projectName, projectDescription);
},
onSuccess: (data) => {
queryClient.invalidateQueries(["recentProjects"]);
toast.success("Project created successfully!");
onClose();
navigate("/data", { state: { datasetId: data.dataset_id, apiData: data } });
},
onError: (error) => {
toast.error("Error creating project: " + error.message);
},
});

const validateForm = () => {
const newErrors = {};
if (!formData.projectName.trim()) {
newErrors.projectName = "Project name is required";
}
if (!formData.projectDescription.trim()) {
newErrors.projectDescription = "Project description is required";
}
if (!fileUpload) {
newErrors.file = "Please select a file to upload";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) return;

uploadMutation.mutate({
file: fileUpload,
projectName: formData.projectName,
projectDescription: formData.projectDescription,
});
};

const handleFileUpload = (event) => {
const file = event.target.files[0];
setFileUpload(file);
};

const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};

if (!isOpen) return null;

return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-8 max-w-md w-full">
<h2 className="text-2xl font-bold mb-6">Create New Project</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Project Name
</label>
<input
type="text"
name="projectName"
value={formData.projectName}
onChange={handleInputChange}
placeholder="Enter project name"
className="w-full px-3 py-2 border border-gray-300 rounded-md"
/>
{errors.projectName && (
<p className="text-red-500 text-sm mt-1">{errors.projectName}</p>
)}
</div>

<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Upload Dataset
</label>
<input
type="file"
onChange={handleFileUpload}
className="w-full cursor-pointer"
/>
{errors.file && (
<p className="text-red-500 text-sm mt-1">{errors.file}</p>
)}
</div>

<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Project Description
</label>
<textarea
name="projectDescription"
value={formData.projectDescription}
onChange={handleInputChange}
placeholder="Enter project description"
className="w-full px-3 py-2 border border-gray-300 rounded-md resize-none h-32"
/>
{errors.projectDescription && (
<p className="text-red-500 text-sm mt-1">{errors.projectDescription}</p>
)}
</div>

<div className="flex justify-end gap-4 mt-6">
<button
type="submit"
disabled={uploadMutation.isLoading}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{uploadMutation.isLoading ? "Creating..." : "Create Project"}
</button>
<button
type="button"
onClick={onClose}
className="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50"
>
Cancel
</button>
</div>
</form>
</div>
</div>
);
};

export default CreateProjectModal;
50 changes: 50 additions & 0 deletions frontend/src/Components/DatasetTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { format } from 'date-fns';

const DatasetTable = ({
data,
onRowClick,
onCreateNew
}) => {
return (
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
<div className="min-w-full divide-y divide-gray-300">
<div className="bg-gray-50 flex justify-between items-center px-6 py-3">
<h2 className="text-lg font-semibold text-gray-900">Datasets</h2>
<button
onClick={onCreateNew}
className="px-4 py-2 bg-gradient-to-r from-green-400 to-blue-500 hover:from-green-500 hover:to-blue-600 text-white rounded-md"
>
Create New Dataset
</button>
</div>
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-900">Name</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-900">Description</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-900">Last Modified</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{data.map((dataset) => (
<tr
key={dataset.dataset_id}
onClick={() => onRowClick(dataset.dataset_id)}
className="hover:bg-gray-50 cursor-pointer"
>
<td className="px-6 py-4 text-sm text-gray-900">{dataset.name}</td>
<td className="px-6 py-4 text-sm text-gray-500">{dataset.description}</td>
<td className="px-6 py-4 text-sm text-gray-500">
{format(new Date(dataset.last_modified), 'PPp')}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};

export default DatasetTable;
Loading