diff --git a/DashAI/front/src/api/datasets.ts b/DashAI/front/src/api/datasets.ts index 813d645ee..eb325a1e4 100644 --- a/DashAI/front/src/api/datasets.ts +++ b/DashAI/front/src/api/datasets.ts @@ -81,7 +81,7 @@ export const updateDataset = async ( export const deleteDataset = async (id: string): Promise => { const response = await api.delete(`${datasetEndpoint}/${id}`); - return response.data; + return response; }; export const getDatasetFile = async (path: string, page = 0, pageSize = 5) => { diff --git a/DashAI/front/src/api/notebook.ts b/DashAI/front/src/api/notebook.ts index 29114fd6d..ed41c413a 100644 --- a/DashAI/front/src/api/notebook.ts +++ b/DashAI/front/src/api/notebook.ts @@ -2,7 +2,6 @@ import api from "./api"; import { INotebook } from "../types/notebook"; import { IExplorer } from "../types/explorer"; import { IConverter } from "../types/converter"; -import { IDataset } from "../types/dataset"; const notebookEndpoint = "/v1/notebook"; @@ -39,8 +38,9 @@ export const getConvertersByNotebookId = async ( return response.data; }; -export const deleteNotebook = async (id: number): Promise => { - await api.delete(`${notebookEndpoint}/${id}`); +export const deleteNotebook = async (id: number): Promise => { + const response = await api.delete(`${notebookEndpoint}/${id}`); + return response; }; export const updateNotebook = async ( diff --git a/DashAI/front/src/components/DatasetVisualization.jsx b/DashAI/front/src/components/DatasetVisualization.jsx index ee84890c9..836811935 100644 --- a/DashAI/front/src/components/DatasetVisualization.jsx +++ b/DashAI/front/src/components/DatasetVisualization.jsx @@ -2,32 +2,23 @@ import { useCallback, useState, useEffect } from "react"; import { Button, Grid, - Paper, Typography, CircularProgress, Box, - Chip, Alert, Divider, Tabs, Tab, } from "@mui/material"; import { useTheme } from "@mui/material/styles"; -import { - AddCircleOutline as AddIcon, - CheckCircle as CheckIcon, -} from "@mui/icons-material"; +import { AddCircleOutline as AddIcon } from "@mui/icons-material"; import { getDatasetFile, getDatasetInfo, getDatasetFileFiltered, } from "../api/datasets"; -import DatasetTable from "./notebooks/dataset/DatasetTable"; -import { getComponents } from "../api/component"; import { useTourContext } from "./tour/TourProvider"; -import { useSnackbar } from "notistack"; import JobQueueWidget from "./jobs/JobQueueWidget"; -import { getDatasetStatus } from "../utils/datasetStatus"; import { formatDate } from "../pages/results/constants/formatDate"; import Header from "./notebooks/dataset/header/Header"; import Tooltip from "@mui/material/Tooltip"; @@ -39,63 +30,59 @@ import CorrelationsTab from "./notebooks/dataset/tabs/CorrelationsTab"; import { QualityAlerts } from "./notebooks/dataset/QualityAlerts"; import { TextTab } from "./notebooks/dataset/tabs/TextTab"; import { useTranslation } from "react-i18next"; - +import { useDatasetsAndNotebooks } from "./custom/contexts/DatasetsAndNotebooksContext"; /** * Component to visualize dataset information including quality metrics, statistics, and data preview. * Can be used across different modules (Notebooks, Models) with customizable action buttons. * @param {Object} props * @param {Object} props.dataset - Dataset object containing id, name, file_path, status, and created date - * @param {Function} props.onItemCreated - Callback function when a new item (notebook/session) is created * @param {Function} props.onNewItem - Callback function when "New Item" button is clicked * @param {string} [props.newItemButtonText="New Item"] - Custom text for the action button (e.g., "New Notebook", "New Session") * @param {Array} [props.existingItems=[]] - Array of existing items (notebooks/sessions) for validation */ export default function DatasetVisualization({ dataset, - onItemCreated, onNewItem, newItemButtonText = "New Item", - existingItems = [], tourContextType = null, }) { const { t } = useTranslation(["datasets", "common"]); const theme = useTheme(); - if (!dataset) { - return ( - - - {t("common:loading")} - - ); - } - const [datasetInfo, setDatasetInfo] = useState(null); const [tab, setTab] = useState(0); const tourContext = useTourContext(); + const status = dataset.status; + const isProcessing = !(status === 3 || status === 4); // Finished or Error + + const { fetchDatasets } = useDatasetsAndNotebooks(); + useEffect(() => { + if (!dataset) return; + setTab(0); const fetchDatasetInfo = async () => { - if (isProcessing) return; - + if (isProcessing) { + setTimeout(() => { + fetchDatasets(); + }, 1000); + return; + } try { - const info = await getDatasetInfo(dataset.id); + const info = await getDatasetInfo(Number(dataset.id)); setDatasetInfo(info); } catch (error) { setDatasetInfo(null); } }; - fetchDatasetInfo(); - }, [dataset.id, dataset.status]); + }, [dataset]); // fetchPage compatible with server-side filtering const fetchDatasetPage = useCallback( async (page, pageSize, filterModel) => { - if (isProcessing) return { rows: [], total: 0 }; + if (!dataset || isProcessing) return { rows: [], total: 0 }; try { // Use getDatasetFile if no filters, else use getDatasetFileFiltered const hasFilters = @@ -118,11 +105,23 @@ export default function DatasetVisualization({ return { rows: [], total: 0 }; } }, - [dataset.file_path, dataset.status, dataset.id], + [ + dataset && dataset.file_path, + dataset && dataset.status, + dataset && dataset.id, + ], ); - const status = dataset.status; - const isProcessing = !(status === 3 || status === 4); // Finished or Error + if (!dataset) { + return ( + + + {t("common:loading")} + + ); + } return ( <> diff --git a/DashAI/front/src/components/custom/contexts/DatasetsAndNotebooksContext.jsx b/DashAI/front/src/components/custom/contexts/DatasetsAndNotebooksContext.jsx new file mode 100644 index 000000000..095aee6ee --- /dev/null +++ b/DashAI/front/src/components/custom/contexts/DatasetsAndNotebooksContext.jsx @@ -0,0 +1,92 @@ +import { createContext, useContext, useEffect, useState } from "react"; + +import { useTranslation } from "react-i18next"; +import { useDatasets } from "../../../hooks/datasets/useDatasets"; +import { useNotebooks } from "../../../hooks/datasets/useNotebooks"; + +const DatasetsAndNotebooksContext = createContext(); + +export const useDatasetsAndNotebooks = () => + useContext(DatasetsAndNotebooksContext); + +export const OptionsEnum = Object.freeze({ + DATASET: "dataset", + NOTEBOOK: "notebook", + NEW: "new", +}); + +export const DatasetsAndNotebooksProvider = ({ children }) => { + const { t } = useTranslation(["datasets", "common"]); + const { + datasets, + createDataset, + selectedDatasetId, + fetchDatasets, + selectDataset, + clearSelectedDataset, + deleteDataset, + deleteDatasetById, + editDataset, + addDatasetOptimistically, + enrichDatasetsWithInfo, + replaceDatasets, + startDatasetPolling, + } = useDatasets({ t }); + + const { + notebooks, + selectedNotebookId, + fetchNotebooks, + selectNotebook, + clearSelectedNotebook, + deleteNotebookById, + editNotebook, + removeNotebooksByDatasetId, + } = useNotebooks({ t }); + + const [step, setStep] = useState(0); + const [selectedOption, setSelectedOption] = useState(OptionsEnum.NEW); // "datasets" or "notebooks" + + const [rightBarContent, setRightBarContent] = useState(null); + + useEffect(() => { + fetchDatasets(); + fetchNotebooks(); + }, []); + + const value = { + datasets, + createDataset, + selectedDatasetId, + fetchDatasets, + selectDataset, + clearSelectedDataset, + deleteDataset, + deleteDatasetById, + editDataset, + addDatasetOptimistically, + enrichDatasetsWithInfo, + replaceDatasets, + startDatasetPolling, + notebooks, + selectedNotebookId, + fetchNotebooks, + selectNotebook, + clearSelectedNotebook, + deleteNotebookById, + editNotebook, + removeNotebooksByDatasetId, + selectedOption, + setSelectedOption, + step, + setStep, + rightBarContent, + setRightBarContent, + }; + + return ( + + {children} + + ); +}; diff --git a/DashAI/front/src/components/generative/ParamsBar.jsx b/DashAI/front/src/components/generative/ParamsBar.jsx index 6496b8939..fd63b156b 100644 --- a/DashAI/front/src/components/generative/ParamsBar.jsx +++ b/DashAI/front/src/components/generative/ParamsBar.jsx @@ -12,7 +12,7 @@ import { updateGenerativeSessionParams, } from "../../api/generativeTask"; import { preprocessSchema, buildYupSchema } from "./utils"; -import SideBar from "../threeSectionLayout/SideBar"; +import SideBar from "../threeSectionLayout/panelContainers/SideBar"; import { useTranslation } from "react-i18next"; export default function ParamsBar({ diff --git a/DashAI/front/src/components/generative/SessionBar.jsx b/DashAI/front/src/components/generative/SessionBar.jsx index 1417ce92b..22b0619a9 100644 --- a/DashAI/front/src/components/generative/SessionBar.jsx +++ b/DashAI/front/src/components/generative/SessionBar.jsx @@ -7,7 +7,7 @@ import InfoSessionModal from "./InfoSessionModal"; import Footer from "./Footer"; import SessionList from "./SessionList"; import NewItemButton from "../threeSectionLayout/NewItemButton"; -import SideBar from "../threeSectionLayout/SideBar"; +import SideBar from "../threeSectionLayout/panelContainers/SideBar"; import BarHeader from "../threeSectionLayout/BarHeader"; import { useTranslation } from "react-i18next"; diff --git a/DashAI/front/src/components/layout/ModuleContainer.jsx b/DashAI/front/src/components/layout/ModuleContainer.jsx new file mode 100644 index 000000000..09f693d7b --- /dev/null +++ b/DashAI/front/src/components/layout/ModuleContainer.jsx @@ -0,0 +1,15 @@ +import { Box } from "@mui/material"; + +export default function ModuleContainer({ children, ...props }) { + return ( + + {children} + + ); +} diff --git a/DashAI/front/src/components/models/LeftBar.jsx b/DashAI/front/src/components/models/LeftBar.jsx index 1109e2ee5..dab468f2c 100644 --- a/DashAI/front/src/components/models/LeftBar.jsx +++ b/DashAI/front/src/components/models/LeftBar.jsx @@ -6,7 +6,7 @@ import StorageIcon from "@mui/icons-material/Storage"; import Biotech from "@mui/icons-material/Biotech"; import Footer from "../threeSectionLayout/Footer"; import BarHeader from "../threeSectionLayout/BarHeader"; -import SideBar from "../threeSectionLayout/SideBar"; +import SideBar from "../threeSectionLayout/panelContainers/SideBar"; import CollapsibleList from "../threeSectionLayout/CollapsibleList"; import GroupedCollapsibleList from "../threeSectionLayout/GroupedCollapsibleList"; import SearchBar from "../threeSectionLayout/SearchBar"; diff --git a/DashAI/front/src/components/models/RightBar.jsx b/DashAI/front/src/components/models/RightBar.jsx index 855abd3d4..a63d2a1fa 100644 --- a/DashAI/front/src/components/models/RightBar.jsx +++ b/DashAI/front/src/components/models/RightBar.jsx @@ -10,7 +10,7 @@ import { import { useTheme } from "@mui/material/styles"; import { ChevronRight, Search as SearchIcon } from "@mui/icons-material"; import { useSnackbar } from "notistack"; -import SideBar from "../threeSectionLayout/SideBar"; +import SideBar from "../threeSectionLayout/panelContainers/SideBar"; import { getComponents } from "../../api/component"; import ModelListItem from "./model/ModelListItem"; import { useTranslation } from "react-i18next"; diff --git a/DashAI/front/src/components/notebooks/LeftBar.jsx b/DashAI/front/src/components/notebooks/DatasetNotebookLeftBar.jsx similarity index 75% rename from DashAI/front/src/components/notebooks/LeftBar.jsx rename to DashAI/front/src/components/notebooks/DatasetNotebookLeftBar.jsx index 8c299c879..fa00bf0af 100644 --- a/DashAI/front/src/components/notebooks/LeftBar.jsx +++ b/DashAI/front/src/components/notebooks/DatasetNotebookLeftBar.jsx @@ -7,29 +7,36 @@ import BarHeader from "../threeSectionLayout/BarHeader"; import CollapsibleList from "../threeSectionLayout/CollapsibleList"; import SearchBar from "../threeSectionLayout/SearchBar"; import NewItemButton from "../threeSectionLayout/NewItemButton"; -import SideBar from "../threeSectionLayout/SideBar"; +import SideBar from "../threeSectionLayout/panelContainers/SideBar"; import InfoNotebookModal from "./notebook/InfoNotebookModal"; import { ChevronLeft } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; -export default function DatasetsNotebooksBar({ - datasets = [], - selectedDatasetId, - notebooks = [], - selectedNotebookId, - onDatasetClick, - onDatasetDelete, - onDatasetEdit, - onNotebookClick, - onNotebookDelete, - onNotebookEdit, - onToggle, - handleNewSessionButton, -}) { - const [searchQuery, setSearchQuery] = useState(""); +import { useDatasetsAndNotebooks } from "../custom/contexts/DatasetsAndNotebooksContext"; + +export default function DatasetsNotebooksLeftBar({ onToggle }) { + const { + datasets, + notebooks, + selectedDatasetId, + selectedNotebookId, + selectDataset, + selectNotebook, + clearSelectedNotebook, + setStep, + setSelectedOption, + clearSelectedDataset, + deleteDatasetById, + removeNotebooksByDatasetId, + editDataset, + editNotebook, + deleteNotebookById, + } = useDatasetsAndNotebooks(); + const [filteredDatasets, setFilteredDatasets] = useState(datasets); const [filteredNotebooks, setFilteredNotebooks] = useState(notebooks); const [selectedInfoNotebook, setSelectedInfoNotebook] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); const { t } = useTranslation(["datasets", "common"]); useEffect(() => { @@ -79,6 +86,48 @@ export default function DatasetsNotebooksBar({ return notebook.description || ""; }; + const onDatasetClick = (id) => { + selectDataset(id); + clearSelectedNotebook(); + setStep(0); + setSelectedOption("dataset"); + }; + + const onNotebookClick = (id) => { + selectNotebook(id); + clearSelectedDataset(); + setStep(0); + setSelectedOption("notebook"); + }; + + const onDatasetDelete = async (id) => { + const success = await deleteDatasetById(id); + if (!success) return; + if (id === selectedDatasetId) { + clearSelectedDataset(); + setStep(0); + setSelectedOption(null); + } + removeNotebooksByDatasetId(id); + }; + + const onNotebookDelete = async (id) => { + const success = await deleteNotebookById(id); + if (!success) return; + if (id === selectedNotebookId) { + clearSelectedNotebook(); + setStep(0); + setSelectedOption(null); + } + }; + + const handleNewSessionButton = () => { + clearSelectedDataset(); + clearSelectedNotebook(); + setStep(0); + setSelectedOption(null); + }; + return ( {/* Header */} @@ -133,7 +182,7 @@ export default function DatasetsNotebooksBar({ selectedItemId={selectedDatasetId} onItemClick={onDatasetClick} onItemDelete={onDatasetDelete} - onItemEdit={onDatasetEdit} + onItemEdit={editDataset} defaultOpen={true} title={t("datasets:label.availableDatasets")} Icon={StorageIcon} @@ -147,7 +196,7 @@ export default function DatasetsNotebooksBar({ selectedItemId={selectedNotebookId} onItemClick={onNotebookClick} onItemDelete={onNotebookDelete} - onItemEdit={onNotebookEdit} + onItemEdit={editNotebook} onItemInfo={handleNotebookInfo} defaultOpen={true} title={t("datasets:label.notebooks")} diff --git a/DashAI/front/src/components/notebooks/RightBar.jsx b/DashAI/front/src/components/notebooks/RightBar.jsx index a74570f06..85084292c 100644 --- a/DashAI/front/src/components/notebooks/RightBar.jsx +++ b/DashAI/front/src/components/notebooks/RightBar.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import SideBar from "../threeSectionLayout/SideBar"; +import SideBar from "../threeSectionLayout/panelContainers/SideBar"; import { Box, Typography, diff --git a/DashAI/front/src/components/notebooks/dataset/DatasetsCenterContent.jsx b/DashAI/front/src/components/notebooks/dataset/DatasetsCenterContent.jsx new file mode 100644 index 000000000..bc356ca4e --- /dev/null +++ b/DashAI/front/src/components/notebooks/dataset/DatasetsCenterContent.jsx @@ -0,0 +1,143 @@ +import { useCallback } from "react"; +import NotebookVisualization from "../notebook/NotebookVisualization"; +import UploadDatasetSteps from "../datasetCreation/UploadDatasetSteps"; +import UploadNotebookSteps from "../notebookCreation/UploadNotebookSteps"; +import DatasetVisualization from "../../DatasetVisualization"; +import SelectOptionMenu from "../../threeSectionLayout/SelectOptionMenu"; +import { useDatasetsAndNotebooks } from "../../custom/contexts/DatasetsAndNotebooksContext"; +import { useTourContext } from "../../tour/TourProvider"; +import { useTranslation } from "react-i18next"; + +export default function DatasetsCenterContent() { + const { + datasets, + notebooks, + selectNotebook, + selectedDatasetId, + selectedNotebookId, + step, + fetchDatasets, + fetchNotebooks, + selectedOption, + setStep, + setSelectedOption, + clearSelectedDataset, + clearSelectedNotebook, + } = useDatasetsAndNotebooks(); + + const tourContext = useTourContext(); + const { t } = useTranslation(["datasets", "common"]); + + const selectedDataset = datasets.find((n) => n.id === selectedDatasetId); + const selectedNotebook = notebooks.find((n) => n.id === selectedNotebookId); + + const goToNextStep = useCallback( + (option) => { + if (option === "dataset") { + setStep(1); + setSelectedOption("dataset"); + } else { + setStep(1); + setSelectedOption("notebook"); + } + + clearSelectedDataset(); + clearSelectedNotebook(); + + if (option === "dataset" && tourContext?.run) { + setTimeout(() => { + tourContext.nextStep(); + }, 600); + } + }, + [tourContext], + ); + + const handleNotebookCreated = async (createdNotebook) => { + await fetchNotebooks(); + setStep(0); + setSelectedOption("notebook"); + selectNotebook(createdNotebook.id); + clearSelectedDataset(); + }; + + const handleNewNotebookFromDataset = () => { + setSelectedOption("notebook"); + setStep(1); + }; + + if (selectedNotebookId && selectedOption === "notebook") { + return ( + + ); + } + + if (selectedDatasetId && selectedOption === "dataset") { + return ( + + ); + } + + if (step === 1 && selectedOption === "dataset") { + return ( + { + setStep(0); + setSelectedOption(null); + fetchDatasets(); + setRightBarContent(null); + }} + /> + ); + } + if (step === 1 && selectedOption === "notebook") { + return ( + { + setStep(0); + setSelectedOption(null); + fetchNotebooks(); + }} + datasets={datasets} + handleNotebookCreated={handleNotebookCreated} + existingNotebooks={notebooks} + preselectedDatasetId={selectedDatasetId} + /> + ); + } + if (step === 0) { + return ( + + ); + } + return null; +} diff --git a/DashAI/front/src/components/notebooks/dataset/header/Header.jsx b/DashAI/front/src/components/notebooks/dataset/header/Header.jsx index b020d2b01..c0215beaa 100644 --- a/DashAI/front/src/components/notebooks/dataset/header/Header.jsx +++ b/DashAI/front/src/components/notebooks/dataset/header/Header.jsx @@ -32,7 +32,7 @@ export default function Header({ flexDirection: "row", gap: 1, justifyContent: "flex-start", - alignItems: "flex-start", + alignItems: "stretch", width: "100%", flexWrap: "wrap", flexGrow: 0, diff --git a/DashAI/front/src/components/notebooks/dataset/header/HeaderBox.jsx b/DashAI/front/src/components/notebooks/dataset/header/HeaderBox.jsx index 3006c9588..823600c92 100644 --- a/DashAI/front/src/components/notebooks/dataset/header/HeaderBox.jsx +++ b/DashAI/front/src/components/notebooks/dataset/header/HeaderBox.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { Box, Typography, Tooltip } from "@mui/material"; import { useTheme } from "@mui/material/styles"; @@ -8,13 +7,12 @@ export function HeaderBox({ title, value, IconComponent, iconColor, bgColor }) { { - if (renderRightBar) { + if (setRightBarContent) { if (step === 1 && Object.entries(selectedDataloader).length !== 0) { - renderRightBar( + setRightBarContent( , ); } else { - renderRightBar(null); + setRightBarContent(null); } } - }, [step, selectedDataloader, existingDatasets, renderRightBar]); + }, [step, selectedDataloader, datasets, setRightBarContent]); + + const handleDatasetCreated = (newDataset, datasetJob) => { + addDatasetOptimistically(newDataset); + setGlobalStep(0); + setSelectedOption("dataset"); + clearSelectedNotebook(); + setRightBarContent(null); + startDatasetPolling(newDataset, datasetJob); + }; return ( { + if (!jobId) return; + + startJobPolling( + jobId, + + //Success + async () => { + enqueueSnackbar( + t("datasets:message.datasetCreationSuccess", { datasetName }), + { variant: "success" }, + ); + + try { + const freshDatasets = await fetchDatasets(true); + const dataset = freshDatasets.find((d) => d.id === datasetId); + + if (dataset) { + const enriched = await enrichDatasetsWithInfo( + freshDatasets, + datasets, + ); + replaceDatasets(enriched); + selectDataset(datasetId); + setStep(0); + setSelectedOption("dataset"); + } else { + await fetchDatasets(); + selectDataset(datasetId); + setStep(0); + setSelectedOption("dataset"); + } + } catch (error) { + console.error("Error after dataset job completion:", error); + await fetchDatasets(); + selectDataset(datasetId); + setStep(0); + setSelectedOption("dataset"); + } + }, + + //Failure + async (result) => { + console.error("Dataset job failed:", result); + + enqueueSnackbar( + t("datasets:error.failedToCreateDataset", { + error: result?.error || t("common:unknownError"), + }), + { variant: "error" }, + ); + + try { + await deleteDataset(datasetId); + } catch (e) { + console.error(e); + } + clearSelectedDataset(); + setStep(0); + setSelectedOption(null); + }, + ); + }; + + const handleAddDatasetFromNotebook = async (name, notebookId) => { + try { + console.log( + "Creating dataset from notebook:", + notebookId, + "with name:", + name, + ); + const dataset = await createDataset(name); + + enqueueSnackbar(t("datasets:message.datasetCreationStarted"), { + variant: "success", + }); + + // optimistic + replaceDatasets((prev) => [...prev, dataset]); + selectDataset(dataset.id); + setStep(0); + setSelectedOption("dataset"); + + const job = await enqueueDatasetJob(dataset.id, null, "", {}, notebookId); + + pollForDataset( + { datasetId: dataset.id, datasetName: name }, + { jobId: job.id }, + ); + } catch (error) { + enqueueSnackbar(t("datasets:error.failedToCreateDatasetFromNotebook"), { + variant: "error", + }); + console.error("Failed to create dataset from notebook:", error); + } + }; + return ( setShowSaveDatasetModal(false)} - onSaveDataset={handleAddDatasetFromNotebook} + onSaveDataset={(name) => + handleAddDatasetFromNotebook(name, notebook.id) + } appliedConverters={converters.filter( (converter) => converter.status === 3, )} diff --git a/DashAI/front/src/components/notebooks/notebook/NotebookVisualization.jsx b/DashAI/front/src/components/notebooks/notebook/NotebookVisualization.jsx index 54ed2b8d0..0f06f84de 100644 --- a/DashAI/front/src/components/notebooks/notebook/NotebookVisualization.jsx +++ b/DashAI/front/src/components/notebooks/notebook/NotebookVisualization.jsx @@ -6,7 +6,6 @@ import JobQueueWidget from "../../jobs/JobQueueWidget"; export default function NotebookVisualization({ notebook, - handleAddDatasetFromNotebook, existingDatasets = [], }) { const [isAccordionExpanded, setIsAccordionExpanded] = useState(true); @@ -21,7 +20,6 @@ export default function NotebookVisualization({ diff --git a/DashAI/front/src/components/threeSectionLayout/CenterBox.jsx b/DashAI/front/src/components/threeSectionLayout/panelContainers/CenterBox.jsx similarity index 100% rename from DashAI/front/src/components/threeSectionLayout/CenterBox.jsx rename to DashAI/front/src/components/threeSectionLayout/panelContainers/CenterBox.jsx diff --git a/DashAI/front/src/components/threeSectionLayout/SideBar.jsx b/DashAI/front/src/components/threeSectionLayout/panelContainers/SideBar.jsx similarity index 100% rename from DashAI/front/src/components/threeSectionLayout/SideBar.jsx rename to DashAI/front/src/components/threeSectionLayout/panelContainers/SideBar.jsx diff --git a/DashAI/front/src/components/threeSectionLayout/panels/CenterPanel.jsx b/DashAI/front/src/components/threeSectionLayout/panels/CenterPanel.jsx new file mode 100644 index 000000000..c245b96db --- /dev/null +++ b/DashAI/front/src/components/threeSectionLayout/panels/CenterPanel.jsx @@ -0,0 +1,21 @@ +import { Box } from "@mui/material"; +import CenterBox from "../panelContainers/CenterBox"; +import { useThreePanelLayoutContext } from "./ThreePanelLayoutContext"; + +export default function CenterPanel({ children }) { + const { centerWidth, isTogglingLeft, isTogglingRight } = + useThreePanelLayoutContext(); + return ( + + {children} + + ); +} diff --git a/DashAI/front/src/components/threeSectionLayout/panels/LeftPanel.jsx b/DashAI/front/src/components/threeSectionLayout/panels/LeftPanel.jsx new file mode 100644 index 000000000..a9b455e6f --- /dev/null +++ b/DashAI/front/src/components/threeSectionLayout/panels/LeftPanel.jsx @@ -0,0 +1,72 @@ +import { Box, IconButton } from "@mui/material"; +import { ChevronRight } from "@mui/icons-material"; +import { useThreePanelLayoutContext } from "./ThreePanelLayoutContext"; + +export default function LeftPanel({ children }) { + const { + leftBarVisible, + leftBarWidth, + isTogglingLeft, + bindLeftResize, + handleToggleLeft, + } = useThreePanelLayoutContext(); + return ( + <> + {!leftBarVisible && ( + + + + )} + + {leftBarVisible && ( + <> + {children} + + + )} + + + ); +} diff --git a/DashAI/front/src/components/threeSectionLayout/panels/RightPanel.jsx b/DashAI/front/src/components/threeSectionLayout/panels/RightPanel.jsx new file mode 100644 index 000000000..c8ee71230 --- /dev/null +++ b/DashAI/front/src/components/threeSectionLayout/panels/RightPanel.jsx @@ -0,0 +1,70 @@ +import { Box, IconButton } from "@mui/material"; +import { ChevronLeft } from "@mui/icons-material"; +import { useThreePanelLayoutContext } from "./ThreePanelLayoutContext"; +export default function RightPanel({ toggleButtonTop = "50%", children }) { + const { + rightBarVisible, + rightBarWidth, + isTogglingRight, + bindRightResize, + handleToggleRight, + } = useThreePanelLayoutContext(); + return ( + <> + {!rightBarVisible && ( + + + + )} + + {rightBarVisible && ( + <> + + {children} + + )} + + + ); +} diff --git a/DashAI/front/src/components/threeSectionLayout/panels/ThreePanelLayoutContext.js b/DashAI/front/src/components/threeSectionLayout/panels/ThreePanelLayoutContext.js new file mode 100644 index 000000000..30213b3b6 --- /dev/null +++ b/DashAI/front/src/components/threeSectionLayout/panels/ThreePanelLayoutContext.js @@ -0,0 +1,12 @@ +import { createContext, useContext } from "react"; + +export const ThreePanelLayoutContext = createContext(null); + +export function useThreePanelLayoutContext() { + const ctx = useContext(ThreePanelLayoutContext); + if (!ctx) + throw new Error( + "useThreePanelLayoutContext must be used within a ThreePanelLayoutProvider", + ); + return ctx; +} diff --git a/DashAI/front/src/hooks/datasets/useDatasets.js b/DashAI/front/src/hooks/datasets/useDatasets.js new file mode 100644 index 000000000..32fcf092e --- /dev/null +++ b/DashAI/front/src/hooks/datasets/useDatasets.js @@ -0,0 +1,162 @@ +import { useState, useCallback } from "react"; +import { useSnackbar } from "notistack"; +import { + getDatasets, + deleteDataset, + getDatasetInfo, + updateDataset, + createDataset, +} from "../../api/datasets"; +import { startJobPolling } from "../../utils/jobPoller"; + +export function useDatasets({ t }) { + const { enqueueSnackbar } = useSnackbar(); + const [datasets, setDatasets] = useState([]); + const [selectedDatasetId, setSelectedDatasetId] = useState(null); + + // ---------------- helpers ---------------- + + const enrichDatasetsWithInfo = useCallback( + async (newDatasets, existingDatasets = []) => { + return Promise.all( + newDatasets.map(async (dataset) => { + const existing = existingDatasets.find((d) => d.id === dataset.id); + + if (existing) { + return { + ...dataset, + total_rows: existing.total_rows, + total_columns: existing.total_columns, + }; + } + + try { + const info = await getDatasetInfo(dataset.id); + return { + ...dataset, + total_rows: info.total_rows, + total_columns: info.total_columns, + }; + } catch (error) { + console.warn( + `Failed to fetch info for dataset ${dataset.id}`, + error, + ); + return dataset; + } + }), + ); + }, + [], + ); + + // ---------------- actions ---------------- + + const fetchDatasets = useCallback(async () => { + const data = await getDatasets(); + const enriched = await enrichDatasetsWithInfo(data, datasets); + setDatasets(enriched); + }, [datasets, enrichDatasetsWithInfo]); + + const selectDataset = (id) => { + setSelectedDatasetId(id); + }; + + const clearSelectedDataset = () => { + setSelectedDatasetId(null); + }; + + const deleteDatasetById = async (id) => { + try { + await deleteDataset(id); + setDatasets((prev) => prev.filter((d) => d.id !== id)); + if (id === selectedDatasetId) { + setSelectedDatasetId(null); + } + return true; + } catch (error) { + enqueueSnackbar(t("datasets:error.failedToDeleteDataset"), { + variant: "error", + }); + console.error("Error deleting dataset:", error); + } + return false; + }; + + const editDataset = async (id, newName) => { + try { + const updated = await updateDataset(id, { name: newName }); + setDatasets((prev) => + prev.map((d) => (d.id === id ? { ...d, name: updated.name } : d)), + ); + enqueueSnackbar(t("datasets:message.datasetUpdateSuccess"), { + variant: "success", + }); + } catch (error) { + if (error.response?.status === 409) { + enqueueSnackbar(t("datasets:error.datasetNameExists"), { + variant: "error", + }); + } else if (error.response?.status === 422) { + enqueueSnackbar(t("datasets:error.datasetNameEmpty"), { + variant: "error", + }); + } else { + enqueueSnackbar(t("datasets:error.failedToUpdateDataset"), { + variant: "error", + }); + } + throw error; + } + }; + + const addDatasetOptimistically = (dataset) => { + setDatasets((prev) => [...prev, dataset]); + setSelectedDatasetId(dataset.id); + }; + + const startDatasetPolling = (newDataset, datasetJob) => { + startJobPolling( + datasetJob.id, + async () => { + enqueueSnackbar( + t("datasets:message.datasetCreationSuccess", { + datasetName: newDataset.name, + }), + { variant: "success" }, + ); + await fetchDatasets(); + setSelectedDatasetId(newDataset.id); + }, + async () => { + enqueueSnackbar(t("datasets:error.failedToCreateDataset"), { + variant: "error", + }); + setDatasets((prev) => prev.filter((d) => d.id !== newDataset.id)); + if (newDataset.id === selectedDatasetId) { + setSelectedDatasetId(null); + } + }, + ); + }; + + const replaceDatasets = (datasets) => { + setDatasets(datasets); + }; + + return { + datasets, + selectedDatasetId, + enrichDatasetsWithInfo, + createDataset, + fetchDatasets, + selectDataset, + clearSelectedDataset, + deleteDataset, + deleteDatasetById, + editDataset, + addDatasetOptimistically, + startDatasetPolling, + replaceDatasets, + }; +} diff --git a/DashAI/front/src/hooks/datasets/useNotebooks.js b/DashAI/front/src/hooks/datasets/useNotebooks.js new file mode 100644 index 000000000..7803f1489 --- /dev/null +++ b/DashAI/front/src/hooks/datasets/useNotebooks.js @@ -0,0 +1,102 @@ +import { useState, useCallback } from "react"; +import { useSnackbar } from "notistack"; +import { + getNotebooks, + deleteNotebook, + updateNotebook, +} from "../../api/notebook"; + +export function useNotebooks({ t }) { + const { enqueueSnackbar } = useSnackbar(); + const [notebooks, setNotebooks] = useState([]); + const [selectedNotebookId, setSelectedNotebookId] = useState(null); + + // -------- actions -------- + + const fetchNotebooks = useCallback(async () => { + try { + const data = await getNotebooks(); + setNotebooks(data); + } catch (error) { + enqueueSnackbar(t("datasets:error.failedToFetchNotebooks"), { + variant: "error", + }); + console.error("Failed to fetch notebooks:", error); + } + }, [enqueueSnackbar, t]); + + const selectNotebook = (id) => { + setSelectedNotebookId(id); + }; + + const clearSelectedNotebook = () => { + setSelectedNotebookId(null); + }; + + const deleteNotebookById = async (id) => { + try { + await deleteNotebook(id); + setNotebooks((prev) => prev.filter((n) => n.id !== id)); + return true; + } catch (error) { + enqueueSnackbar(t("datasets:error.failedToDeleteNotebook"), { + variant: "error", + }); + console.error("Error deleting notebook:", error); + } + return false; + }; + + const editNotebook = async (id, newName) => { + try { + const updated = await updateNotebook(id, { name: newName }); + setNotebooks((prev) => + prev.map((n) => (n.id === id ? { ...n, name: updated.name } : n)), + ); + enqueueSnackbar(t("datasets:message.notebookUpdateSuccess"), { + variant: "success", + }); + } catch (error) { + if (error.response?.status === 422) { + enqueueSnackbar(t("datasets:error.notebookNameEmpty"), { + variant: "error", + }); + } else if (error.response?.status === 304) { + enqueueSnackbar(t("datasets:message.noChangesMade"), { + variant: "info", + }); + } else { + enqueueSnackbar(t("datasets:error.failedToUpdateNotebook"), { + variant: "error", + }); + } + throw error; + } + }; + + const removeNotebooksByDatasetId = (datasetId) => { + setNotebooks((prev) => prev.filter((n) => n.dataset_id !== datasetId)); + + if ( + selectedNotebookId && + notebooks.find( + (n) => n.id === selectedNotebookId && n.dataset_id === datasetId, + ) + ) { + setSelectedNotebookId(null); + } + }; + + return { + notebooks, + selectedNotebookId, + + fetchNotebooks, + selectNotebook, + clearSelectedNotebook, + deleteNotebookById, + editNotebook, + + removeNotebooksByDatasetId, + }; +} diff --git a/DashAI/front/src/hooks/useThreePanelsLayout.js b/DashAI/front/src/hooks/useThreePanelsLayout.js new file mode 100644 index 000000000..cc1041bc3 --- /dev/null +++ b/DashAI/front/src/hooks/useThreePanelsLayout.js @@ -0,0 +1,104 @@ +import { useState, useRef, useEffect, useCallback } from "react"; + +export function useThreePanelLayout() { + const [leftBarVisible, setLeftBarVisible] = useState(true); + const [rightBarVisible, setRightBarVisible] = useState(true); + const [leftBarWidth, setLeftBarWidth] = useState(20); + const [rightBarWidth, setRightBarWidth] = useState(20); + + const [isTogglingLeft, setIsTogglingLeft] = useState(false); + const [isTogglingRight, setIsTogglingRight] = useState(false); + + const isResizingLeft = useRef(false); + const isResizingRight = useRef(false); + + const handleMouseMove = useCallback((e) => { + const container = document.querySelector('[data-container="datasets"]'); + if (!container) return; + + const rect = container.getBoundingClientRect(); + + if (isResizingLeft.current) { + const w = ((e.clientX - rect.left) / rect.width) * 100; + if (w >= 15 && w <= 40) setLeftBarWidth(w); + } + + if (isResizingRight.current) { + const w = ((rect.right - e.clientX) / rect.width) * 100; + if (w >= 15 && w <= 40) setRightBarWidth(w); + } + }, []); + + const handleMouseUp = useCallback(() => { + isResizingLeft.current = false; + isResizingRight.current = false; + document.body.style.cursor = "default"; + document.body.style.userSelect = "auto"; + }, []); + + useEffect(() => { + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [handleMouseMove, handleMouseUp]); + + const handleToggleLeft = useCallback(() => { + setIsTogglingLeft(true); + setLeftBarVisible((v) => !v); + + setTimeout(() => { + setIsTogglingLeft(false); + }, 300); + }, []); + + const handleToggleRight = useCallback(() => { + setIsTogglingRight(true); + setRightBarVisible((v) => !v); + + setTimeout(() => { + setIsTogglingRight(false); + }, 300); + }, []); + + const centerWidth = + leftBarVisible && rightBarVisible + ? 100 - leftBarWidth - rightBarWidth + : leftBarVisible + ? 100 - leftBarWidth + : rightBarVisible + ? 100 - rightBarWidth + : 100; + + return { + leftBarVisible, + rightBarVisible, + leftBarWidth, + rightBarWidth, + centerWidth, + + handleToggleLeft, + handleToggleRight, + + isTogglingLeft, + isTogglingRight, + + bindLeftResize: { + onMouseDown: () => { + isResizingLeft.current = true; + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + }, + }, + + bindRightResize: { + onMouseDown: () => { + isResizingRight.current = true; + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + }, + }, + }; +} diff --git a/DashAI/front/src/pages/datasets/Datasets.jsx b/DashAI/front/src/pages/datasets/Datasets.jsx index f244049a0..140814de8 100644 --- a/DashAI/front/src/pages/datasets/Datasets.jsx +++ b/DashAI/front/src/pages/datasets/Datasets.jsx @@ -1,11 +1,14 @@ import { TourProvider } from "../../components/tour/TourProvider"; import { TOUR_KEYS } from "../../constants/tours"; +import { DatasetsAndNotebooksProvider } from "../../components/custom/contexts/DatasetsAndNotebooksContext"; import DatasetsContent from "./DatasetsContent"; export default function DatasetsPage() { return ( - + + + ); } diff --git a/DashAI/front/src/pages/datasets/DatasetsContent.jsx b/DashAI/front/src/pages/datasets/DatasetsContent.jsx index 077b0a001..341b2023a 100644 --- a/DashAI/front/src/pages/datasets/DatasetsContent.jsx +++ b/DashAI/front/src/pages/datasets/DatasetsContent.jsx @@ -1,789 +1,75 @@ -import { useState, useEffect, useRef, useCallback } from "react"; -import { Box, IconButton } from "@mui/material"; -import { ChevronLeft, ChevronRight } from "@mui/icons-material"; -import { useSnackbar } from "notistack"; -import { useTourContext } from "../../components/tour/TourProvider"; -import LeftBar from "../../components/notebooks/LeftBar"; -import CenterBox from "../../components/threeSectionLayout/CenterBox"; +import ModuleContainer from "../../components/layout/ModuleContainer"; +import LeftPanel from "../../components/threeSectionLayout/panels/LeftPanel"; +import DatasetsNotebooksLeftBar from "../../components/notebooks/DatasetNotebookLeftBar"; +import CenterPanel from "../../components/threeSectionLayout/panels/CenterPanel"; import RightBar from "../../components/notebooks/RightBar"; -import SelectOptionMenu from "../../components/threeSectionLayout/SelectOptionMenu"; -import UploadDatasetSteps from "../../components/notebooks/datasetCreation/UploadDatasetSteps"; -import UploadNotebookSteps from "../../components/notebooks/notebookCreation/UploadNotebookSteps"; -import DatasetVisualization from "../../components/DatasetVisualization"; -import NotebookVisualization from "../../components/notebooks/notebook/NotebookVisualization"; -import { - getDatasets, - deleteDataset, - getDatasetInfo, - updateDataset, - createDataset, -} from "../../api/datasets"; +import RightPanel from "../../components/threeSectionLayout/panels/RightPanel"; +import DatasetsCenterContent from "../../components/notebooks/dataset/DatasetsCenterContent"; import { TourProvider } from "../../components/tour/TourProvider"; import { TourButton } from "../../components/tour/TourButton"; import { TOUR_KEYS } from "../../constants/tours"; -import { startJobPolling } from "../../utils/jobPoller"; -import { - getNotebooks, - deleteNotebook, - updateNotebook, -} from "../../api/notebook"; -import { enqueueDatasetJob } from "../../api/job"; import { ExplorersAndConvertersProvider } from "../../components/notebooks/context/ExplorersAndConvertersContext"; -import { useTranslation } from "react-i18next"; +import { useThreePanelLayout } from "../../hooks/useThreePanelsLayout"; +import { ThreePanelLayoutContext } from "../../components/threeSectionLayout/panels/ThreePanelLayoutContext"; +import { useDatasetsAndNotebooks } from "../../components/custom/contexts/DatasetsAndNotebooksContext"; export default function DatasetsContent() { - const [step, setStep] = useState(0); - const [selectedOption, setSelectedOption] = useState(null); - const [selectedDatasetId, setSelectedDatasetId] = useState(null); - const [selectedNotebookId, setSelectedNotebookId] = useState(0); - const [datasets, setDatasets] = useState([]); - const [notebooks, setNotebooks] = useState([]); - const [leftBarVisible, setLeftBarVisible] = useState(true); - const [rightBarVisible, setRightBarVisible] = useState(true); - const [leftBarWidth, setLeftBarWidth] = useState(20); - const [rightBarWidth, setRightBarWidth] = useState(20); - const [rightBarContent, setRightBarContent] = useState(null); - const isResizingLeft = useRef(false); - const isResizingRight = useRef(false); - const [isTogglingLeft, setIsTogglingLeft] = useState(false); - const [isTogglingRight, setIsTogglingRight] = useState(false); - const tourContext = useTourContext(); - const { enqueueSnackbar } = useSnackbar(); - const { t } = useTranslation(["datasets", "common"]); + const threePanelLayout = useThreePanelLayout(); - const goToNextStep = (option) => { - if (option === "dataset" && tourContext?.run) { - setStep((prevStep) => prevStep + 1); - setSelectedOption(option); - setSelectedNotebookId(null); - setSelectedDatasetId(null); - setTimeout(() => { - tourContext.nextStep(); - }, 600); - } else { - setStep((prevStep) => prevStep + 1); - setSelectedOption(option); - setSelectedNotebookId(null); - setSelectedDatasetId(null); - } - }; + const { notebooks, selectedNotebookId, rightBarContent } = + useDatasetsAndNotebooks(); - const enrichDatasetsWithInfo = async (newDatasets, existingDatasets = []) => { - const enrichedDatasets = await Promise.all( - newDatasets.map(async (dataset) => { - const existingDataset = existingDatasets.find( - (d) => d.id === dataset.id, - ); - if (existingDataset) { - return { - ...dataset, - total_rows: existingDataset.total_rows, - total_columns: existingDataset.total_columns, - }; - } - - try { - const info = await getDatasetInfo(dataset.id); - return { - ...dataset, - total_rows: info.total_rows, - total_columns: info.total_columns, - }; - } catch (error) { - console.warn( - `Failed to fetch info for dataset ${dataset.id}:`, - error, - ); - return { - ...dataset, - total_rows: dataset.total_rows, - columns: dataset.total_columns, - }; - } - }), - ); - return enrichedDatasets; - }; - - const fetchDatasets = async () => { - try { - const data = await getDatasets(); - const enrichedDatasets = await enrichDatasetsWithInfo(data, datasets); - setDatasets(enrichedDatasets); - } catch (error) { - enqueueSnackbar(t("datasets:error.failedToFetchDatasets"), { - variant: "error", - }); - console.error("Failed to fetch datasets:", error); - } - }; - - const fetchNotebooks = async () => { - try { - const data = await getNotebooks(); - setNotebooks(data); - } catch (error) { - enqueueSnackbar(t("datasets:error.failedToFetchNotebooks"), { - variant: "error", - }); - console.error("Failed to fetch notebooks:", error); - } - }; - - useEffect(() => { - fetchDatasets(); - fetchNotebooks(); - }, []); - - const handleNewSessionButton = () => { - setSelectedDatasetId(null); - setSelectedNotebookId(null); - setStep(0); - setSelectedOption(null); - }; - - const handleDatasetClick = (datasetId) => { - setSelectedDatasetId(datasetId); - setSelectedNotebookId(null); - setSelectedOption("dataset"); - setRightBarContent(null); - }; - - const handleNotebookClick = (notebookId) => { - setSelectedNotebookId(notebookId); - setSelectedDatasetId(null); - setSelectedOption("notebook"); - }; - - const handleDatasetDelete = (id) => { - if (id === selectedDatasetId) { - setSelectedDatasetId(null); - setStep(0); - setSelectedOption(null); - } - - setDatasets((prevDatasets) => - prevDatasets.filter((dataset) => dataset.id !== id), - ); - - setNotebooks((prevNotebooks) => { - const filteredNotebooks = prevNotebooks.filter( - (notebook) => notebook.dataset_id !== id, - ); - - if ( - selectedNotebookId && - prevNotebooks.find( - (notebook) => - notebook.id === selectedNotebookId && notebook.dataset_id === id, - ) - ) { - setSelectedNotebookId(null); - setStep(0); - setSelectedOption(null); - } - - return filteredNotebooks; - }); - - deleteDataset(id); - }; - - const handleNotebookDelete = (id) => { - if (id === selectedNotebookId) { - setSelectedNotebookId(null); - setStep(0); - setSelectedOption(null); - } - - setNotebooks((prevNotebooks) => - prevNotebooks.filter((notebook) => notebook.id !== id), - ); - - deleteNotebook(id); - }; - - const handleAddDatasetFromNotebook = async (name) => { - if (selectedNotebook) { - try { - const data = await createDataset(name); - enqueueSnackbar(t("datasets:message.datasetCreationStarted"), { - variant: "success", - }); - setDatasets((prev) => [...prev, data]); - setSelectedDatasetId(data.id); - setSelectedOption("dataset"); - setSelectedNotebookId(null); - - const job = await enqueueDatasetJob( - data.id, - null, - "", - {}, - selectedNotebook.id, - ); - pollForDataset( - { datasetId: data.id, datasetName: name }, - { jobId: job.id }, - ); - } catch (error) { - enqueueSnackbar(t("datasets:error.failedToCreateDatasetFromNotebook"), { - variant: "error", - }); - console.error("Failed to create dataset from notebook:", error); - } - } - }; - - const handleNotebookCreated = async (createdNotebook) => { - await fetchNotebooks(); - setStep(0); - setSelectedOption("notebook"); - setSelectedNotebookId(createdNotebook.id); - setSelectedDatasetId(null); - }; - - const handleNewNotebookFromDataset = () => { - // Keep selectedDatasetId but go to notebook creation - setSelectedOption("notebook"); - setStep(1); - }; - - const handleDatasetCreated = async (newDataset, datasetJob) => { - setDatasets((prevDatasets) => [...prevDatasets, newDataset]); - setSelectedDatasetId(newDataset.id); - - setStep(0); - setSelectedOption("dataset"); - setSelectedNotebookId(null); - - // clear right bar content injected during dataset creation (e.g. dataloader config) - setRightBarContent(null); - - pollForDataset( - { datasetId: newDataset.id, datasetName: newDataset.name }, - { jobId: datasetJob.id }, - ); - }; - - const pollForDataset = async ( - { datasetId, datasetName }, - { jobId }, - attempt = 1, - maxAttempts = 10, - ) => { - if (jobId && attempt === 1) { - startJobPolling( - jobId, - async (result) => { - enqueueSnackbar( - t("datasets:message.datasetCreationSuccess", { datasetName }), - { - variant: "success", - }, - ); - - try { - const freshDatasets = await getDatasets(); - const dataset = freshDatasets.find((d) => d.id === datasetId); - - if (dataset) { - const enrichedDatasets = await enrichDatasetsWithInfo( - freshDatasets, - datasets, - ); - setDatasets(enrichedDatasets); - setSelectedDatasetId(datasetId); - } else { - await fetchDatasets(); - setSelectedDatasetId(datasetId); - } - } catch (error) { - console.error( - "Error fetching datasets after job completion:", - error, - ); - await fetchDatasets(); - setSelectedDatasetId(datasetId); - } - }, - (result) => { - console.error(`Dataset job failed:`, result); - enqueueSnackbar( - t("datasets:error.failedToCreateDataset", { - error: result.error || t("common:unknownError"), - }), - { variant: "error" }, - ); - - setDatasets((prevDatasets) => { - const datasetExists = prevDatasets.some((d) => d.id === datasetId); - - if (datasetExists) { - deleteDataset(datasetId).catch((error) => { - console.error("Error deleting failed dataset:", error); - }); - - return prevDatasets.filter((d) => d.id !== datasetId); - } - - return prevDatasets; - }); - - setSelectedDatasetId(null); - setStep(0); - setSelectedOption(null); - }, - ); - } - }; - - const handleEditDataset = async (id, newName) => { - try { - const updatedDataset = await updateDataset(id, { name: newName }); - setDatasets((prevDatasets) => - prevDatasets.map((dataset) => - dataset.id === id - ? { ...dataset, name: updatedDataset.name } - : dataset, - ), - ); - enqueueSnackbar(t("datasets:message.datasetUpdateSuccess"), { - variant: "success", - }); - } catch (error) { - console.error("Failed to update dataset:", error); - if (error.response?.status === 409) { - enqueueSnackbar(t("datasets:error.datasetNameExists"), { - variant: "error", - }); - } else if (error.response?.status === 422) { - enqueueSnackbar(t("datasets:error.datasetNameEmpty"), { - variant: "error", - }); - } else { - enqueueSnackbar(t("datasets:error.failedToUpdateDataset"), { - variant: "error", - }); - } - throw error; - } - }; - - const handleEditNotebook = async (id, newName) => { - try { - const updatedNotebook = await updateNotebook(id, { name: newName }); - setNotebooks((prevNotebooks) => - prevNotebooks.map((notebook) => - notebook.id === id - ? { ...notebook, name: updatedNotebook.name } - : notebook, - ), - ); - enqueueSnackbar(t("datasets:message.notebookUpdateSuccess"), { - variant: "success", - }); - } catch (error) { - console.error("Failed to update notebook:", error); - if (error.response?.status === 422) { - enqueueSnackbar(t("datasets:error.notebookNameEmpty"), { - variant: "error", - }); - } else if (error.response?.status === 304) { - enqueueSnackbar(t("datasets:message.noChangesMade"), { - variant: "info", - }); - } else { - enqueueSnackbar(t("datasets:error.failedToUpdateNotebook"), { - variant: "error", - }); - } - throw error; - } - }; - - const handleMouseMove = useCallback((e) => { - if (isResizingLeft.current) { - const container = document.querySelector('[data-container="datasets"]'); - const containerRect = container.getBoundingClientRect(); - const newWidth = - ((e.clientX - containerRect.left) / containerRect.width) * 100; - if (newWidth >= 15 && newWidth <= 40) { - setLeftBarWidth(newWidth); - } - } - - if (isResizingRight.current) { - const container = document.querySelector('[data-container="datasets"]'); - const containerRect = container.getBoundingClientRect(); - const newWidth = - ((containerRect.right - e.clientX) / containerRect.width) * 100; - if (newWidth >= 15 && newWidth <= 40) { - setRightBarWidth(newWidth); - } - } - }, []); - - const handleMouseUp = useCallback(() => { - isResizingLeft.current = false; - isResizingRight.current = false; - document.body.style.cursor = "default"; - document.body.style.userSelect = "auto"; - }, []); - - const handleToggleLeft = () => { - setIsTogglingLeft(true); - setLeftBarVisible(!leftBarVisible); - setTimeout(() => setIsTogglingLeft(false), 300); - }; - - const handleToggleRight = () => { - setIsTogglingRight(true); - setRightBarVisible(!rightBarVisible); - setTimeout(() => setIsTogglingRight(false), 300); - }; - - useEffect(() => { - window.addEventListener("mousemove", handleMouseMove); - window.addEventListener("mouseup", handleMouseUp); - return () => { - window.removeEventListener("mousemove", handleMouseMove); - window.removeEventListener("mouseup", handleMouseUp); - }; - }, [handleMouseMove, handleMouseUp]); - - const centerWidth = - leftBarVisible && rightBarVisible - ? 100 - leftBarWidth - rightBarWidth - : leftBarVisible - ? 100 - leftBarWidth - : rightBarVisible - ? 100 - rightBarWidth - : 100; - - const selectedDataset = datasets.find((n) => n.id === selectedDatasetId); const selectedNotebook = notebooks.find((n) => n.id === selectedNotebookId); return ( - <> - - {/* Left Panel */} - - {leftBarVisible && ( - <> - - { - isResizingLeft.current = true; - document.body.style.cursor = "col-resize"; - document.body.style.userSelect = "none"; - }} - sx={{ - position: "absolute", - right: -2, - top: 0, - bottom: 0, - width: "5px", - cursor: "col-resize", - bgcolor: "transparent", - transition: "background-color 0.2s ease", - "&:hover": { - bgcolor: "primary.main", - }, - zIndex: 10, - }} - /> - - )} - - - {!leftBarVisible && ( - - - - )} + + + + + {selectedNotebookId ? ( <> - {/* Center Panel - Notebook */} - - - + + + + {rightBarContent ? ( + rightBarContent + ) : ( + - - - - {!rightBarVisible && ( - - - - )} - - {/* Right Panel - Notebook */} - - {rightBarVisible && ( - <> - { - isResizingRight.current = true; - document.body.style.cursor = "col-resize"; - document.body.style.userSelect = "none"; - }} - sx={{ - position: "absolute", - left: -2, - top: 0, - bottom: 0, - width: "5px", - cursor: "col-resize", - bgcolor: "transparent", - transition: "background-color 0.2s ease", - "&:hover": { - bgcolor: "primary.main", - }, - zIndex: 10, - }} - /> - - )} - - + ) : ( <> - - - {step === 1 && selectedOption === "dataset" ? ( - { - setStep(0); - setSelectedOption(null); - fetchDatasets(); - // clear right bar when exiting - setRightBarContent(null); - }} - handleDatasetCreated={handleDatasetCreated} - existingDatasets={datasets} - renderRightBar={setRightBarContent} - /> - ) : step === 1 && selectedOption === "notebook" ? ( - { - setStep(0); - setSelectedOption(null); - fetchNotebooks(); - }} - datasets={datasets} - handleNotebookCreated={handleNotebookCreated} - existingNotebooks={notebooks} - preselectedDatasetId={selectedDatasetId} - /> - ) : selectedDatasetId ? ( - - ) : step === 0 ? ( - - ) : null} - - - - {!rightBarVisible && ( - - - - )} - - - {rightBarVisible && ( - <> - { - isResizingRight.current = true; - document.body.style.cursor = "col-resize"; - document.body.style.userSelect = "none"; - }} - sx={{ - position: "absolute", - left: -2, - top: 0, - bottom: 0, - width: "5px", - cursor: "col-resize", - bgcolor: "transparent", - transition: "background-color 0.2s ease", - "&:hover": { - bgcolor: "primary.main", - }, - zIndex: 10, - }} - /> - {rightBarContent ? ( - rightBarContent - ) : ( - - )} - + + + + + {rightBarContent ? ( + rightBarContent + ) : ( + )} - + )} - + {!selectedNotebookId && } - + ); } diff --git a/DashAI/front/src/pages/generative/Generative.jsx b/DashAI/front/src/pages/generative/Generative.jsx index d058d0a40..bba366248 100644 --- a/DashAI/front/src/pages/generative/Generative.jsx +++ b/DashAI/front/src/pages/generative/Generative.jsx @@ -6,8 +6,7 @@ import GenerativeChat from "../../components/generative/GenerativeChat"; import SelectModelMenu from "../../components/generative/SelectModelMenu"; import ParamsBar from "../../components/generative/ParamsBar"; import { getSessions, removeSession } from "../../api/session"; -import JobQueueWidget from "../../components/jobs/JobQueueWidget"; -import CenterBox from "../../components/threeSectionLayout/CenterBox"; +import CenterBox from "../../components/threeSectionLayout/panelContainers/CenterBox"; import { getComponents } from "../../api/component"; import { useTranslation } from "react-i18next"; diff --git a/DashAI/front/src/pages/models/ModelsContent.jsx b/DashAI/front/src/pages/models/ModelsContent.jsx index 2afc32d6d..135f2a740 100644 --- a/DashAI/front/src/pages/models/ModelsContent.jsx +++ b/DashAI/front/src/pages/models/ModelsContent.jsx @@ -8,7 +8,7 @@ import { TourProvider } from "../../components/tour/TourProvider"; import { TourButton } from "../../components/tour/TourButton"; import { TOUR_KEYS } from "../../constants/tours"; import LeftBar from "../../components/models/LeftBar"; -import CenterBox from "../../components/threeSectionLayout/CenterBox"; +import CenterBox from "../../components/threeSectionLayout/panelContainers/CenterBox"; import RightBar from "../../components/models/RightBar"; import SelectOptionMenu from "../../components/threeSectionLayout/SelectOptionMenu"; import CreateSessionSteps from "../../components/models/CreateSessionSteps"; diff --git a/DashAI/front/src/utils/i18n/locales/en/datasets.json b/DashAI/front/src/utils/i18n/locales/en/datasets.json index c490ed673..eb1e03b88 100644 --- a/DashAI/front/src/utils/i18n/locales/en/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/en/datasets.json @@ -30,6 +30,8 @@ "failedToCreateDataset": "Error creating dataset: {{error}}", "failedToCreateDatasetFromNotebook": "Failed to create dataset from notebook", "failedToCreateExplorer": "Failed to create explorer", + "failedToDeleteDataset": "Failed to delete dataset", + "failedToDeleteNotebook": "Failed to delete notebook", "failedToFetchDatasets": "Failed to fetch datasets", "failedToFetchNotebooks": "Failed to fetch notebooks", "failedToLoadDatasetInfo": "Failed to fetch dataset info", diff --git a/DashAI/front/src/utils/i18n/locales/es/datasets.json b/DashAI/front/src/utils/i18n/locales/es/datasets.json index e34a1e647..819dca52c 100644 --- a/DashAI/front/src/utils/i18n/locales/es/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/es/datasets.json @@ -30,6 +30,8 @@ "failedToCreateDataset": "Error al crear dataset: {{error}}", "failedToCreateDatasetFromNotebook": "Fallo al crear dataset desde cuaderno", "failedToCreateExplorer": "Fallo al crear explorador", + "failedToDeleteDataset": "Fallo al eliminar dataset", + "failedToDeleteNotebook": "Fallo al eliminar cuaderno", "failedToFetchDatasets": "Fallo al obtener datasets", "failedToFetchNotebooks": "Fallo al obtener cuadernos", "failedToLoadDatasetInfo": "Fallo al obtener información del dataset",