Skip to content

Commit

Permalink
Merge pull request #6 from ABI-Deployment-Thesis/ruiar/add-cpu-and-me…
Browse files Browse the repository at this point in the history
…mory

feat(ui): add docker tag, cpu and memory, UI enhancements
  • Loading branch information
ruigomes99 authored Aug 27, 2024
2 parents 271f593 + 8015f45 commit ee612e6
Show file tree
Hide file tree
Showing 12 changed files with 378 additions and 73 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<h2 align="center">ABI Deployment Thesis - Web Application</h2>

Welcome to the abi-deployment-webapp repository! This project is a key part of a master's thesis at the University of Minho. It's a Proof of Concept for a proposed architecture designed to deploy and integrate intelligent models within ABI (Adaptive Business Intelligence) systems.
Welcome to the abi-deployment-webapp repository! This project is a key part of a master's thesis at the University of Minho. It's a Proof of Concept for a proposed architecture designed to deploy and integrate intelligent models within Adaptive Business Intelligence (ABI) systems.

**This repository provides a web application that serves as the user interface for interacting with the PoC.**

Expand Down
79 changes: 65 additions & 14 deletions src/components/AddModelModal/AddModelModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ function AddModelModal({ show, handleClose, refreshModels }) {
const [type, setType] = useState('predictive');
const [engine, setEngine] = useState('docker');
const [language, setLanguage] = useState('Python3');
const [dockerTag, setDockerTag] = useState('3.9');
const [serialization, setSerialization] = useState('joblib');
const [features, setFeatures] = useState([{ name: '', type: 'int' }]);
const [dependencies, setDependencies] = useState([{ library: '', version: '' }]);
const [memLimit, setMemLimit] = useState('256M');
const [cpuPercentage, setCpuPercentage] = useState(50);
const [file, setFile] = useState(null);
const [error, setError] = useState(null);
const [formError, setFormError] = useState(null);

const handleAddFeature = () => setFeatures([...features, { name: '', type: 'int' }]);
const handleRemoveFeature = (index) => setFeatures(features.filter((_, i) => i !== index));
Expand All @@ -23,15 +26,26 @@ function AddModelModal({ show, handleClose, refreshModels }) {

const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
const allowedExtensions = ['.py', '.sav', '.rds', '.zip'];
const allowedExtensions = ['.pkl', '.sav', '.rds', '.zip'];
const fileExtension = selectedFile ? `.${selectedFile.name.split('.').pop()}` : '';

if (allowedExtensions.includes(fileExtension)) {
setFile(selectedFile);
setError(null);
setFormError(null);
} else {
setFile(null);
setError('Only .py, .sav, .rds, and .zip files are allowed.');
setFormError('Only .pkl, .sav, .rds, and .zip files are allowed.');
}
};

const handleLanguageChange = (e) => {
const selectedLanguage = e.target.value;
setLanguage(selectedLanguage);

if (selectedLanguage === 'R') {
setDockerTag('4.1.3');
} else if (selectedLanguage === 'Python3') {
setDockerTag('3.9');
}
};

Expand All @@ -45,6 +59,9 @@ function AddModelModal({ show, handleClose, refreshModels }) {

if (engine === 'docker') {
formData.append('language', language);
formData.append('docker_tag', dockerTag);
formData.append('mem_limit', memLimit);
formData.append('cpu_percentage', cpuPercentage);
}

if (type === 'predictive') {
Expand All @@ -65,7 +82,7 @@ function AddModelModal({ show, handleClose, refreshModels }) {
handleClose();
} catch (error) {
const errorMessage = error.response?.data?.error;
setError(
setFormError(
Array.isArray(errorMessage)
? errorMessage.map(err => err.msg).join(', ')
: errorMessage || 'An unexpected error occurred.'
Expand All @@ -79,7 +96,6 @@ function AddModelModal({ show, handleClose, refreshModels }) {
<Modal.Title>Add New Model</Modal.Title>
</Modal.Header>
<Modal.Body>
{error && <Alert variant="danger">{error}</Alert>}
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3">
<Form.Label>Name</Form.Label>
Expand All @@ -98,18 +114,51 @@ function AddModelModal({ show, handleClose, refreshModels }) {
<Form.Label>Engine</Form.Label>
<Form.Select value={engine} onChange={(e) => setEngine(e.target.value)}>
<option value="docker">Docker</option>
<option value="test">Test</option>
</Form.Select>
</Form.Group>

{engine === 'docker' && (
<Form.Group className="mb-3">
<Form.Label>Language</Form.Label>
<Form.Select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="Python3">Python3</option>
<option value="R">R</option>
</Form.Select>
</Form.Group>
<>
<Form.Group className="mb-3">
<Form.Label>Language</Form.Label>
<Form.Select value={language} onChange={(e) => handleLanguageChange(e)}>
<option value="Python3">Python3</option>
{type === 'predictive' && <option value="R">R</option>}
</Form.Select>
</Form.Group>

<Form.Group className="mb-3">
<Form.Label>Docker Tag</Form.Label>
<Form.Control
type="text"
value={dockerTag}
onChange={(e) => setDockerTag(e.target.value)}
required
/>
</Form.Group>

<Form.Group className="mb-3">
<Form.Label>Memory Limit</Form.Label>
<Form.Control
type="text"
value={memLimit}
onChange={(e) => setMemLimit(e.target.value)}
placeholder="e.g., 256M"
required
/>
</Form.Group>

<Form.Group className="mb-3">
<Form.Label>CPU Percentage</Form.Label>
<Form.Control
type="number"
value={cpuPercentage}
onChange={(e) => setCpuPercentage(e.target.value)}
placeholder="e.g., 50"
required
/>
</Form.Group>
</>
)}

{type === 'predictive' && engine === 'docker' && language === 'Python3' && (
Expand Down Expand Up @@ -200,6 +249,8 @@ function AddModelModal({ show, handleClose, refreshModels }) {
<Form.Control type="file" onChange={handleFileChange} required />
</Form.Group>

{formError && <Alert variant="danger">{formError}</Alert>}

<Button variant="primary" type="submit">Submit</Button>
</Form>
</Modal.Body>
Expand Down
27 changes: 27 additions & 0 deletions src/components/ModelRunDetailsModal/ModelRunDetailsModal.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
/* src/components/ModelRunDetailsModal/ModelRunDetailsModal.css */

.custom-modal-width {
max-width: 80%;
/* Adjust the percentage as needed */
width: 800px;
/* Or set a fixed width */
}

.state-queue {
color: gray;
}

.state-building {
color: #0d6efd;
}

.state-running {
color: orange;
}

.state-finished {
color: green;
}

.state-failed {
color: red;
}

.modal-body {
word-wrap: break-word;
/* Ensure long words break and wrap */
}
75 changes: 48 additions & 27 deletions src/components/ModelRunDetailsModal/ModelRunDetailsModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,31 @@ const ModelRunDetailsModal = ({ show, handleClose, runId }) => {
const [runDetails, setRunDetails] = useState(null);
const [modelType, setModelType] = useState('');

useEffect(() => {
const fetchRunDetails = async () => {
const token = localStorage.getItem('token');
try {
const { data: runData } = await axios.get(`${API_ENDPOINTS.MODEL_RUNS}/${runId}`, {
headers: { Authorization: `Bearer ${token}` },
});
const fetchRunDetails = async () => {
const token = localStorage.getItem('token');
try {
const { data: runData } = await axios.get(`${API_ENDPOINTS.MODEL_RUNS}/${runId}`, {
headers: { Authorization: `Bearer ${token}` },
});

const { data: modelData } = await axios.get(`${API_ENDPOINTS.MODEL}/${runData.model_id}`, {
headers: { Authorization: `Bearer ${token}` },
});
const { data: modelData } = await axios.get(`${API_ENDPOINTS.MODELS}/${runData.model_id}`, {
headers: { Authorization: `Bearer ${token}` },
});

setRunDetails(runData);
setModelType(modelData.type);
} catch (error) {
console.error('Error fetching run details:', error);
setRunDetails(null);
}
};
setRunDetails(runData);
setModelType(modelData.type);
} catch (error) {
console.error('Error fetching run details:', error);
setRunDetails(null);
}
};

useEffect(() => {
if (show && runId) {
fetchRunDetails();
const intervalId = setInterval(fetchRunDetails, 3000); // Update every 3 seconds

return () => clearInterval(intervalId); // Clean up interval on component unmount
}
}, [show, runId]);

Expand All @@ -47,6 +50,8 @@ const ModelRunDetailsModal = ({ show, handleClose, runId }) => {
));
};

const stateClass = runDetails ? `state-${runDetails.state}` : '';

return (
<Modal show={show} onHide={handleClose} dialogClassName="custom-modal-width">
<Modal.Header closeButton>
Expand All @@ -55,18 +60,34 @@ const ModelRunDetailsModal = ({ show, handleClose, runId }) => {
<Modal.Body>
{runDetails ? (
<>
<h5>Model ID: {runDetails.model_id}</h5>
<h5>State: {runDetails.state}</h5>
<h5>Created At: {new Date(runDetails.createdAt).toLocaleString()}</h5>
<h5>Updated At: {new Date(runDetails.updatedAt).toLocaleString()}</h5>
<h5>Container ID: {runDetails.container_id}</h5>
<h5>Container Exit Code: {runDetails.container_exit_code}</h5>
<h5>Result:</h5>
<pre>{runDetails.result}</pre>
<strong>Model ID:</strong> {runDetails.model_id}<br />
<strong>State: <span className={stateClass}>{runDetails.state}</span></strong><br />
<strong>Created At:</strong> {new Date(runDetails.createdAt).toLocaleString()}<br />
<strong>Updated At:</strong> {new Date(runDetails.updatedAt).toLocaleString()}<br />
{runDetails.container_id !== undefined && runDetails.container_id !== null && runDetails.container_id !== '' && (
<>
<strong>Container ID:</strong> {runDetails.container_id}<br />
</>
)}

{(runDetails.state == 'failed' || runDetails.state == 'finished') && (
<>
<strong>Container Exit Code:</strong> {runDetails.container_exit_code}<br />
</>
)}
<br />

{(runDetails.state == 'failed' || runDetails.state == 'finished') && (
<>
<strong>Result:</strong>
<pre>{runDetails.result}</pre>
</>
)}

{modelType !== 'optimization' && (
<>
<h5>Input Features:</h5>
<Table bordered>
<strong>Input Features:</strong>
<Table>
<thead>
<tr>
<th>Name</th>
Expand Down
45 changes: 39 additions & 6 deletions src/components/RunModelModal/RunModelModal.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// src/components/RunModelModal/RunModelModal.jsx
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Modal, Button, Form } from 'react-bootstrap';
import React, { useState, useEffect, useCallback } from 'react';
import { Modal, Button, Form, Alert } from 'react-bootstrap';
import axios from 'axios';
import { API_ENDPOINTS } from '../../config/config';

Expand All @@ -12,6 +12,7 @@ const RunModelModal = ({ show, handleClose, refreshModelRuns }) => {
const [inputValues, setInputValues] = useState({});
const [file, setFile] = useState(null);
const [fileError, setFileError] = useState('');
const [formError, setFormError] = useState('');

const token = localStorage.getItem('token');

Expand All @@ -22,7 +23,12 @@ const RunModelModal = ({ show, handleClose, refreshModelRuns }) => {
});
setModels(response.data);
} catch (error) {
console.error('Error fetching models:', error);
const errorMessage = error.response?.data?.error;
setFormError(
Array.isArray(errorMessage)
? errorMessage.map(err => err.msg).join(', ')
: errorMessage || 'Failed to load models. Please try again.'
);
}
}, [token]);

Expand All @@ -40,7 +46,12 @@ const RunModelModal = ({ show, handleClose, refreshModelRuns }) => {
setFeatures(features);
setModelType(type);
} catch (error) {
console.error('Error fetching model details:', error);
const errorMessage = error.response?.data?.error;
setFormError(
Array.isArray(errorMessage)
? errorMessage.map(err => err.msg).join(', ')
: errorMessage || 'Failed to load model details. Please try again.'
);
}
}
}, [selectedModel, token]);
Expand All @@ -54,6 +65,7 @@ const RunModelModal = ({ show, handleClose, refreshModelRuns }) => {
setInputValues({});
setFile(null);
setFileError('');
setFormError('');
};

const handleInputChange = (feature, event) => {
Expand Down Expand Up @@ -105,7 +117,7 @@ const RunModelModal = ({ show, handleClose, refreshModelRuns }) => {
model_id: selectedModel,
input_features: features.map(feature => ({
name: feature.name,
value: inputValues[feature.name] == '0' ? 0 : (inputValues[feature.name] || '')
value: inputValues[feature.name] === '0' || inputValues[feature.name] === '0.0' ? 0 : (inputValues[feature.name] || '')
}))
};
headers['Content-Type'] = 'application/json';
Expand All @@ -125,10 +137,25 @@ const RunModelModal = ({ show, handleClose, refreshModelRuns }) => {
handleClose();
refreshModelRuns();
} catch (error) {
console.error('Error running model:', error);
const errorMessage = error.response?.data?.error;
setFormError(
Array.isArray(errorMessage)
? errorMessage.map(err => err.msg).join(', ')
: errorMessage || 'Failed to run the model. Please try again.'
);
}
};

useEffect(() => {
if (show) {
const params = new URLSearchParams(window.location.search);
const modelId = params.get('model_id');
if (modelId && models.length > 0) {
setSelectedModel(modelId);
}
}
}, [show, models]);

return (
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
Expand All @@ -146,6 +173,8 @@ const RunModelModal = ({ show, handleClose, refreshModelRuns }) => {
</Form.Control>
</Form.Group>

{selectedModel && <br />}

{modelType === 'predictive' && features.map(feature => (
<Form.Group controlId={`feature-${feature.name}`} key={feature._id}>
<Form.Label>{feature.name}</Form.Label>
Expand All @@ -168,6 +197,10 @@ const RunModelModal = ({ show, handleClose, refreshModelRuns }) => {
</Form.Group>
)}

<br />

{formError && <Alert variant="danger">{formError}</Alert>}

<Button variant="primary" type="submit">Run</Button>
</Form>
</Modal.Body>
Expand Down
Loading

0 comments on commit ee612e6

Please sign in to comment.