diff --git a/backend/core/views.py b/backend/core/views.py index 097e9efa..932302a1 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -91,6 +91,14 @@ def create(self, validated_data): existing_trainings = Training.objects.filter(model_id=model_id).exclude( status__in=["FINISHED", "FAILED"] ) + model = get_object_or_404(Model, id=model_id) + if not Label.objects.filter( + aoi__in=AOI.objects.filter(dataset=model.dataset) + ).exists(): + raise ValidationError( + "Error: No labels associated with the model, Create AOI & Labels for Dataset" + ) + if existing_trainings.exists(): raise ValidationError( "Another training is already running or submitted for this model." diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js b/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js index edb78cba..d5a535a9 100644 --- a/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js @@ -11,6 +11,8 @@ import { useNavigate, useParams } from "react-router-dom"; import { modelStatus } from "../../../../utils"; import axios from "../../../../axios"; import { useMutation, useQuery } from "react-query"; +import Popup from "./Popup"; + import OSMUser from "../../../Shared/OSMUser"; import SaveIcon from "@material-ui/icons/Save"; import { Checkbox, FormControlLabel } from "@mui/material"; @@ -25,6 +27,8 @@ const AIModelEditor = (props) => { const [error, setError] = useState(null); const [epochs, setEpochs] = useState(20); const [zoomLevel, setZoomLevel] = useState([19]); + const [popupOpen, setPopupOpen] = useState(false); + const [popupRowData, setPopupRowData] = useState(null); const [random, setRandom] = useState(Math.random()); const [batchSize, setBatchSize] = useState(8); @@ -45,6 +49,20 @@ const AIModelEditor = (props) => { } finally { } }; + const openPopupWithTrainingId = async (trainingId) => { + try { + const response = await axios.get(`/training/${trainingId}/`); + setPopupRowData(response.data); + setPopupOpen(true); + } catch (error) { + console.error("Error fetching training information:", error); + } + }; + const handlePopupOpen = (row) => { + setPopupRowData(row); + setPopupOpen(true); + }; + const { data, isLoading, refetch } = useQuery("getModelById", getModelById, { refetchInterval: 60000, }); @@ -92,23 +110,43 @@ const AIModelEditor = (props) => { Model ID: {data.id} - - Status: {modelStatus(data.status)} - {data.published_training && - ", training: " + data.published_training + ", "} + +
+ Status: {modelStatus(data.status)} + {data.published_training && ( + <> + , + { + e.preventDefault(); + openPopupWithTrainingId(data.published_training); + }} + color="inherit" + > + Training: {data.published_training} + + + )} +
{data.status === 0 && ( - { e.preventDefault(); navigate("/start-mapping/" + data.id); }} - color="inherit" > Start mapping - + )}
+ { )} + {popupRowData && ( + setPopupOpen(false)} + row={popupRowData} + /> + )} ); }; diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/Popup.js b/frontend/src/components/Layout/AIModels/AIModelEditor/Popup.js index 114c315b..96a717aa 100644 --- a/frontend/src/components/Layout/AIModels/AIModelEditor/Popup.js +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/Popup.js @@ -5,6 +5,7 @@ import { DialogContent, DialogActions, Button, + CircularProgress, } from "@mui/material"; import axios from "../../../../axios"; @@ -12,6 +13,7 @@ const Popup = ({ open, handleClose, row }) => { const [error, setError] = useState(null); const [traceback, setTraceback] = useState(null); const [imageUrl, setImageUrl] = useState(null); + const [loading, setLoading] = useState(false); const getTrainingStatus = async (taskId) => { try { @@ -60,10 +62,13 @@ const Popup = ({ open, handleClose, row }) => { }; useEffect(() => { + setLoading(true); if (row.status === "FAILED" || row.status === "RUNNING") { - getTrainingStatus(row.task_id); + getTrainingStatus(row.task_id).finally(() => setLoading(false)); } else if (row.status === "FINISHED") { - getDatasetId(row.model); + getDatasetId(row.model).finally(() => setLoading(false)); + } else { + setLoading(false); } }, [row.status, row.task_id, row.model]); @@ -95,32 +100,52 @@ const Popup = ({ open, handleClose, row }) => {

Accuracy: {row.accuracy}

+

+ Status: {row.status} +

{(row.status === "FAILED" || row.status === "RUNNING") && ( <> -

- Status: {row.status} -

- {traceback && ( -
- {renderTraceback()} + {loading ? ( +
+
+ ) : ( + traceback && ( +
+ {renderTraceback()} +
+ ) )} )} - {row.status === "FINISHED" && imageUrl && ( -
- training graph -
+ {row.status === "FINISHED" && ( + <> + {loading ? ( +
+ +
+ ) : ( + imageUrl && ( +
+ training graph +
+ ) + )} + )} diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/Trainings.js b/frontend/src/components/Layout/AIModels/AIModelEditor/Trainings.js index b4456bc2..ab4ab86a 100644 --- a/frontend/src/components/Layout/AIModels/AIModelEditor/Trainings.js +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/Trainings.js @@ -1,6 +1,13 @@ import React, { useContext, useEffect, useState } from "react"; import { DataGrid } from "@mui/x-data-grid"; -import { Grid, IconButton, Tooltip, Typography } from "@mui/material"; +import { + Grid, + IconButton, + Tooltip, + Typography, + CircularProgress, +} from "@mui/material"; + import { useMutation, useQuery } from "react-query"; import axios from "../../../../axios"; import Alert from "@material-ui/lab/Alert"; @@ -23,6 +30,8 @@ const DEFAULT_FILTER = { const AIModelsList = (props) => { const [error, setError] = useState(null); const [popupOpen, setPopupOpen] = useState(false); + const [publishing, setPublishing] = useState(false); + const [popupRow, setPopupRow] = useState(null); const handlePopupOpen = (row) => { @@ -61,6 +70,13 @@ const AIModelsList = (props) => { field: "description", headerName: "Description", flex: 1, + renderCell: (params) => { + return ( + + {params.value} + + ); + }, }, { field: "epochs", @@ -172,13 +188,16 @@ const AIModelsList = (props) => { mutate(params.row.id); }} > - + {publishing ? ( + + ) : ( + + )} )} ); - //

{`${params.row.status} and id ${params.row.id}`}

; }, }, ]; @@ -191,6 +210,7 @@ const AIModelsList = (props) => { const { accessToken } = useContext(AuthContext); const publishModel = async (trainingId) => { + setPublishing(true); try { const headers = { "access-token": accessToken, @@ -213,8 +233,10 @@ const AIModelsList = (props) => { console.log("isError"); setError(JSON.stringify(e)); } finally { + setPublishing(false); } }; + const { mutate } = useMutation(publishModel); return ( <> diff --git a/frontend/src/components/Layout/AIModels/AIModelsList/AIModelsList.js b/frontend/src/components/Layout/AIModels/AIModelsList/AIModelsList.js index e9e6163e..7e2d7d3a 100644 --- a/frontend/src/components/Layout/AIModels/AIModelsList/AIModelsList.js +++ b/frontend/src/components/Layout/AIModels/AIModelsList/AIModelsList.js @@ -51,6 +51,14 @@ const AIModelsList = (props) => { { field: "name", headerName: "Name", + width: 250, + renderCell: (params) => { + return ( + + {params.value} + + ); + }, }, { field: "created_at", @@ -79,6 +87,7 @@ const AIModelsList = (props) => { { field: "dataset", headerName: "Dataset ID", + width: 150, renderCell: (params) => { return {params.value}; diff --git a/frontend/src/components/Layout/Start/ModelsMap/ModelsMap.js b/frontend/src/components/Layout/Start/ModelsMap/ModelsMap.js index 6bf3b87d..ce1960f2 100644 --- a/frontend/src/components/Layout/Start/ModelsMap/ModelsMap.js +++ b/frontend/src/components/Layout/Start/ModelsMap/ModelsMap.js @@ -47,7 +47,18 @@ const PublishedAIModelsList = (props) => { ); }, }, - { field: "name", headerName: "Name" }, + { + field: "name", + headerName: "Name", + width: 250, + renderCell: (params) => { + return ( + + {params.value} + + ); + }, + }, { field: "last_modified", headerName: "Last Modified", diff --git a/frontend/src/components/Layout/Start/Prediction/Prediction.js b/frontend/src/components/Layout/Start/Prediction/Prediction.js index 6aa9a00f..31d70427 100644 --- a/frontend/src/components/Layout/Start/Prediction/Prediction.js +++ b/frontend/src/components/Layout/Start/Prediction/Prediction.js @@ -17,6 +17,8 @@ import { GeoJSON } from "react-leaflet"; const Prediction = () => { const { id } = useParams(); const [error, setError] = useState(false); + const [apiCallInProgress, setApiCallInProgress] = useState(false); + const [map, setMap] = useState(null); const [zoom, setZoom] = useState(0); const [responseTime, setResponseTime] = useState(0); @@ -28,6 +30,20 @@ const Prediction = () => { ]); const [josmEnabled, setJosmEnabled] = useState(false); + useEffect(() => { + if (!apiCallInProgress) { + return; + } + + const timer = setInterval(() => { + setResponseTime((prevResponseTime) => prevResponseTime + 1); + }, 1000); + + return () => { + clearInterval(timer); + }; + }, [apiCallInProgress]); + useEffect(() => { getModel(); }, []); @@ -80,6 +96,7 @@ const Prediction = () => { data: predictions, isLoading: predictionLoading, } = useMutation(async () => { + setApiCallInProgress(true); const headers = { "access-token": accessToken, }; @@ -98,6 +115,7 @@ const Prediction = () => { const res = await axios.post(`/prediction/`, body, { headers }); const endTime = new Date().getTime(); // measure end time setResponseTime((endTime - startTime) / 1000); // calculate and store response time in seconds + setApiCallInProgress(false); if (res.error) { setError( `${res.error.response.statusText}, ${JSON.stringify( diff --git a/frontend/src/components/Layout/TrainingDS/DatasetList/DatasetList.js b/frontend/src/components/Layout/TrainingDS/DatasetList/DatasetList.js index afbbfa3a..0ea4e679 100644 --- a/frontend/src/components/Layout/TrainingDS/DatasetList/DatasetList.js +++ b/frontend/src/components/Layout/TrainingDS/DatasetList/DatasetList.js @@ -52,6 +52,13 @@ const DatasetList = (props) => { field: "name", headerName: "Name", width: 250, + renderCell: (params) => { + return ( + + {params.value} + + ); + }, }, { field: "created_at",