Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web UI - stage1, readonly part #606

Merged
merged 50 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
040d446
added fastapi as web ui backend
VukW Jul 11, 2024
97e6bd7
Added cube + benchmark basic listing
VukW Jul 12, 2024
0382684
Adds navigation
VukW Jul 15, 2024
55fe60e
Aded mlcube detailed page
VukW Jul 19, 2024
fb1bca3
Improved mlcubes detailed layout
VukW Jul 19, 2024
64cf53e
Improved mlcube layout
VukW Jul 19, 2024
36611e1
yaml displaying
VukW Jul 19, 2024
56fa5c4
yaml: spinner
VukW Jul 19, 2024
8563887
yaml panel improvement
VukW Jul 19, 2024
07ce4ab
yaml panel layout improvement
VukW Jul 19, 2024
b260401
layout fixes
VukW Jul 19, 2024
b7980a8
Added benchmark detailed page
VukW Jul 19, 2024
ca356cc
added links to mlcube
VukW Jul 19, 2024
6efd724
benchmark page: added owner
VukW Jul 19, 2024
319b1bf
Colors refactoring
VukW Jul 19, 2024
58008f3
Dataset detailed page
VukW Jul 23, 2024
375d89e
Forgot to add js file
VukW Jul 23, 2024
c6d8a56
Unified data format for all data fields automatically
VukW Jul 23, 2024
74f7743
(mlcube-detailed) Display image tarball and additional files always
VukW Jul 24, 2024
b312882
Fixed scrolling and reinvented basic page layout
VukW Jul 24, 2024
0e282cb
Fix navbar is hiding
VukW Jul 24, 2024
6b28ebb
Make templates & static files independent of user's workdir
VukW Jul 29, 2024
881b281
Added error handling
VukW Jul 29, 2024
e28107b
Display invalid entities correctly
VukW Jul 30, 2024
5b718eb
Added invalid entities highlighting + badges
VukW Jul 30, 2024
0f95027
Added benchmark associations
VukW Aug 5, 2024
444786e
Improved association panel style
VukW Aug 5, 2024
e273577
Added association card
VukW Aug 6, 2024
eea1e77
Sorted associations by status / timestamp
VukW Aug 6, 2024
7b68911
Sorted mlcubes and datasets: mine first
VukW Aug 6, 2024
8251c42
Added associations to dataset page
VukW Aug 7, 2024
b669358
Added associations to mlcube page
VukW Aug 7, 2024
039f496
Refactored details page - extracted common styles to the base template
VukW Aug 10, 2024
c225a5e
Refactored association sorting to common util
VukW Aug 10, 2024
ad0451f
Display my benchmarks first
VukW Aug 10, 2024
12ffef2
Hid empty links
VukW Aug 12, 2024
cedad96
Mlcube-as-a-link unified view
VukW Aug 12, 2024
3ac8a74
resources.path cannot return a dir with subdirs for py3.9
VukW Aug 13, 2024
6170b53
Fixed resources path for templates also
VukW Aug 14, 2024
53b557b
linter fix
VukW Aug 14, 2024
2b73c4f
static local resources instead of remote ones
VukW Aug 26, 2024
75d6776
layout fix: align mlcubes vertically
VukW Aug 27, 2024
c47a751
bugfix: add some dependencies for isolated run
VukW Aug 27, 2024
d837837
Merge branch 'main' into web-ui
VukW Aug 27, 2024
c58efd8
Fixes after merging main
VukW Aug 28, 2024
8e73e54
MedperfSchema requires a name field
VukW Sep 17, 2024
a78ef8d
Linter fix
VukW Sep 17, 2024
1384b21
Pass mlcube params instead of url
aristizabal95 Oct 8, 2024
64d8b3c
Pass mlcube parameters to fetch-yaml
aristizabal95 Oct 8, 2024
7d6f01a
Merge pull request #9 from aristizabal95/web-ui-fetch-yaml
VukW Oct 9, 2024
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
2 changes: 2 additions & 0 deletions cli/medperf/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import medperf.commands.association.association as association
import medperf.commands.compatibility_test.compatibility_test as compatibility_test
import medperf.commands.storage as storage
import medperf.web_ui.app as web_ui
from medperf.utils import check_for_updates
from medperf.logging.utils import log_machine_details

Expand All @@ -30,6 +31,7 @@
app.add_typer(compatibility_test.app, name="test", help="Manage compatibility tests")
app.add_typer(auth.app, name="auth", help="Authentication")
app.add_typer(storage.app, name="storage", help="Storage management")
app.add_typer(web_ui.app, name="web-ui", help="local web UI to manage medperf entities")


@app.command("run")
Expand Down
2 changes: 2 additions & 0 deletions cli/medperf/commands/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ def run(
"""Lists all local datasets

Args:
entity_class: entity class to instantiate (Dataset, Model, etc.)
fields (list[str]): list of fields to display
local_only (bool, optional): Display all local results. Defaults to False.
mine_only (bool, optional): Display all current-user results. Defaults to False.
kwargs (dict): Additional parameters for filtering entity lists.
Expand Down
Empty file.
38 changes: 38 additions & 0 deletions cli/medperf/web_ui/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import typer
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles

from medperf import config
from medperf.decorators import clean_except
from medperf.web_ui.datasets.routes import router as datasets_router
from medperf.web_ui.benchmarks.routes import router as benchmarks_router
from medperf.web_ui.mlcubes.routes import router as mlcubes_router
from medperf.web_ui.yaml_fetch.routes import router as yaml_fetch_router

web_app = FastAPI()

web_app.include_router(datasets_router, prefix="/datasets")
web_app.include_router(benchmarks_router, prefix="/benchmarks")
web_app.include_router(mlcubes_router, prefix="/mlcubes")
web_app.include_router(yaml_fetch_router)

web_app.mount("/static", StaticFiles(directory="medperf/web_ui/static"), name="static")


@web_app.get("/", include_in_schema=False)
def read_root():
return RedirectResponse(url="/benchmarks/ui")


app = typer.Typer()


@app.command("run")
@clean_except
def run(
port: int = typer.Option(8100, "--port", help="port to use"),
):
"""Runs a local web UI"""
import uvicorn
uvicorn.run(web_app, host="127.0.0.1", port=port, log_level=config.loglevel)
Empty file.
47 changes: 47 additions & 0 deletions cli/medperf/web_ui/benchmarks/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# medperf/web-ui/benchmarks/routes.py
import logging

from fastapi import APIRouter, HTTPException
from fastapi.responses import HTMLResponse
from fastapi import Request
from fastapi.templating import Jinja2Templates

from medperf.entities.benchmark import Benchmark
from medperf.entities.cube import Cube
from medperf.account_management import get_medperf_user_data

router = APIRouter()
templates = Jinja2Templates(directory="medperf/web_ui/templates")
logger = logging.getLogger(__name__)


@router.get("/ui", response_class=HTMLResponse)
def benchmarks_ui(request: Request, local_only: bool = False, mine_only: bool = False):
filters = {}
if mine_only:
filters["owner"] = get_medperf_user_data()["id"]

benchmarks = Benchmark.all(
local_only=local_only,
filters=filters,
)
return templates.TemplateResponse("benchmarks.html", {"request": request, "benchmarks": benchmarks})


@router.get("/ui/{benchmark_id}", response_class=HTMLResponse)
def benchmark_detail_ui(request: Request, benchmark_id: int):
benchmark = Benchmark.get(benchmark_id)
data_preparation_mlcube = Cube.get(cube_uid=benchmark.data_preparation_mlcube)
reference_model_mlcube = Cube.get(cube_uid=benchmark.reference_model_mlcube)
metrics_mlcube = Cube.get(cube_uid=benchmark.data_evaluator_mlcube)

return templates.TemplateResponse(
"benchmark_detail.html",
{
"request": request,
"benchmark": benchmark,
"data_preparation_mlcube": data_preparation_mlcube,
"reference_model_mlcube": reference_model_mlcube,
"metrics_mlcube": metrics_mlcube
}
)
Empty file.
44 changes: 44 additions & 0 deletions cli/medperf/web_ui/datasets/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import logging

from fastapi import APIRouter, HTTPException
from fastapi.responses import HTMLResponse
from typing import List
from fastapi import Request
from fastapi.templating import Jinja2Templates

from medperf.account_management import get_medperf_user_data
from medperf.entities.cube import Cube
from medperf.entities.dataset import Dataset

router = APIRouter()

templates = Jinja2Templates(directory="medperf/web_ui/templates")

logger = logging.getLogger(__name__)


@router.get("/", response_model=List[Dataset])
def get_datasets(local_only: bool = False, mine_only: bool = False):
filters = {}
if mine_only:
filters["owner"] = get_medperf_user_data()["id"]

return Dataset.all(
local_only=local_only,
filters=filters,
)


@router.get("/ui", response_class=HTMLResponse)
def datasets_ui(request: Request, local_only: bool = False, mine_only: bool = False):
datasets = get_datasets(local_only, mine_only)
return templates.TemplateResponse("datasets.html", {"request": request, "datasets": datasets})


@router.get("/ui/{dataset_id}", response_class=HTMLResponse)
def dataset_detail_ui(request: Request, dataset_id: int):
dataset = Dataset.get(dataset_id)

prep_cube = Cube.get(cube_uid=dataset.data_preparation_mlcube)
prep_cube_name = prep_cube.name if prep_cube else "Unknown"
return templates.TemplateResponse("dataset_detail.html", {"request": request, "dataset": dataset, "prep_cube_name": prep_cube_name})
Empty file.
32 changes: 32 additions & 0 deletions cli/medperf/web_ui/mlcubes/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging

from fastapi import APIRouter, HTTPException
from fastapi.responses import HTMLResponse
from fastapi import Request
from fastapi.templating import Jinja2Templates

from medperf.entities.cube import Cube
from medperf.account_management import get_medperf_user_data

router = APIRouter()
templates = Jinja2Templates(directory="medperf/web_ui/templates")
logger = logging.getLogger(__name__)


@router.get("/ui", response_class=HTMLResponse)
def mlcubes_ui(request: Request, local_only: bool = False, mine_only: bool = False):
filters = {}
if mine_only:
filters["owner"] = get_medperf_user_data()["id"]

mlcubes = Cube.all(
local_only=local_only,
filters=filters,
)
return templates.TemplateResponse("mlcubes.html", {"request": request, "mlcubes": mlcubes})


@router.get("/ui/{mlcube_id}", response_class=HTMLResponse)
def mlcube_detail_ui(request: Request, mlcube_id: int):
mlcube = Cube.get(cube_uid=mlcube_id)
return templates.TemplateResponse("mlcube_detail.html", {"request": request, "mlcube": mlcube})
Empty file added cli/medperf/web_ui/prompt.md
Empty file.
115 changes: 115 additions & 0 deletions cli/medperf/web_ui/templates/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<!-- ./cli/medperf/web-ui/templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}MedPerf{% endblock %}</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css">
<style>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to put this in a separate css file? It feels like this has too much content to be inserted as an inline style instruction

.badge-state-operational {
background-color: lightgreen;
color: black;
}
.badge-state-development {
background-color: lightcoral;
color: black;
}
.badge-approval-pending {
background-color: coral;
color: black;
}
.badge-approval-approved {
background-color: lightgreen;
color: black;
}
.badge-approval-rejected {
background-color: red;
color: black;
}
.spinner {
display: inline-block;
width: 80px;
height: 80px;
border: 3px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: #007bff;
animation: spin 1s ease-in-out infinite;
margin: 0 auto;
}
.page-container {
display: flex;
justify-content: space-between;
align-items: flex-start;
height: 100vh; /* Ensure the parent container takes full viewport height */
overflow: hidden; /* Prevent horizontal scrolling on the whole page */
}

.main-content-wrapper {
flex: 1;
display: flex;
justify-content: center; /* Center the main content horizontally */
align-items: flex-start; /* Align items to the top */
width: 100%; /* Take all the available width */
}

.main-content {
/*max-width: 800px;*/
min-width: 0; /* Allow the main content to shrink below its content size */
transition: margin-right 0.3s;
}

#yaml-content {
flex: 0 0 40%;
max-width: 40%;
min-width: 300px; /* Set a reasonable minimum width */
height: 100%; /* Take full height of the parent container */
overflow-y: auto; /* Enable vertical scrolling within this container */
overflow-x: auto; /* Enable horizontal scrolling within this container if needed */
}

@keyframes spin {
to {
transform: rotate(360deg);
}
}
pre {
white-space: pre-wrap;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">MedPerf</a>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/benchmarks/ui">Benchmarks</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/mlcubes/ui">MLCubes</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/datasets/ui">Datasets</a>
</li>
</ul>
</div>
</nav>
<div class="page-container d-flex mt-4">
<div class="main-content-wrapper">
<div class="container main-content">
{% block content %}{% endblock %}
</div>
<div class="container mt-4" id="yaml-content" style="display: none;">
<h5>YAML Content</h5>
<div id="loading-indicator" class="spinner" style="display: none;"></div>
<pre><code id="yaml-code" class="language-yaml"></code></pre>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/components/prism-yaml.min.js"></script>
<script src="/static/js/common.js"></script>
</body>
</html>
58 changes: 58 additions & 0 deletions cli/medperf/web_ui/templates/benchmark_detail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!-- ./cli/medperf/web-ui/templates/benchmark_detail.html -->
{% extends "base.html" %}

{% block title %}Benchmark Details{% endblock %}

{% block content %}
<style>
.card-body.d-flex {
flex-wrap: wrap;
}

.card-text {
word-wrap: break-word;
white-space: normal;
}
</style>
<h1 class="my-4">{{ benchmark.name }}</h1>
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="card-title mb-0">Details</h5>
<span class="badge {% if benchmark.state == 'OPERATION' %}badge-state-operational{% else %}badge-state-development{% endif %}">
{{ benchmark.state }}
</span>
<span class="badge {% if benchmark.approval_status == 'APPROVED' %}badge-approval-approved{% elif benchmark.approval_status == 'PENDING' %}badge-approval-pending{% else %}badge-approval-rejected{% endif %}">
{{ benchmark.approval_status }}
</span>
</div>
<div class="card mb-3">
<div class="card-body">
<p class="card-text"><strong>Description:</strong> {{ benchmark.description }}</p>
<p class="card-text"><strong>Documentation:</strong> <a href="{{ benchmark.docs_url }}" class="text-primary" target="_blank">{{ benchmark.docs_url }}</a></p>
<p class="card-text"><strong>Demo Dataset Tarball:</strong> <a href="{{ benchmark.demo_dataset_tarball_url }}" class="text-primary" target="_blank">{{ benchmark.demo_dataset_tarball_url }}</a></p>
<p class="card-text text-muted small">{{ benchmark.demo_dataset_tarball_hash }}</p>
</div>
</div>
<div class="card mb-3">
<div class="card-body d-flex justify-content-between flex-wrap">
<div class="w-50">
<p class="card-text"><strong>Data Preparation MLCube:</strong> <a href="/mlcubes/ui/{{ data_preparation_mlcube.id }}" class="text-primary">{{ data_preparation_mlcube.name }}</a></p>
<p class="card-text"><strong>Reference Model MLCube:</strong> <a href="/mlcubes/ui/{{ reference_model_mlcube.id }}" class="text-primary">{{ reference_model_mlcube.name }}</a></p>
<p class="card-text"><strong>Metrics MLCube:</strong> <a href="/mlcubes/ui/{{ metrics_mlcube.id }}" class="text-primary">{{ metrics_mlcube.name }}</a></p>
<p class="card-text"><strong>Owner:</strong> <i class="fas fa-user"></i> <span class="text-muted small">{{ benchmark.owner }}</span></p>
</div>
<div class="text-right w-50">
<p class="card-text"><strong>Created:</strong> <span class="text-muted small" id="created-at">{{ benchmark.created_at }}</span></p>
<p class="card-text"><strong>Modified:</strong> <span class="text-muted small" id="modified-at">{{ benchmark.modified_at }}</span></p>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
formatDates();
});
</script>
{% endblock %}
27 changes: 27 additions & 0 deletions cli/medperf/web_ui/templates/benchmarks.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{% extends "base.html" %}

{% block title %}Benchmarks{% endblock %}

{% block content %}
<h1 class="my-4">Benchmarks</h1>
<div class="row">
{% for benchmark in benchmarks %}
<div class="col-md-4 mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title"><a href="/benchmarks/ui/{{ benchmark.id }}" class="text-primary">{{ benchmark.name }}</a></h5>
<h6 class="card-subtitle mb-2 text-muted">{{ benchmark.state }}</h6>
<p class="card-text">{{ benchmark.description }}</p>
<p class="card-text"><a href="{{ benchmark.docs_url }}" class="card-link">Documentation</a></p>
<p class="card-text"><small class="text-muted">Created At: {{ benchmark.created_at }}</small></p>
<p class="card-text"><small class="text-muted">Data Preparation MLCube: {{ benchmark.data_preparation_mlcube }}</small></p>
<p class="card-text"><small class="text-muted">Reference Model MLCube: {{ benchmark.reference_model_mlcube }}</small></p>
<p class="card-text"><small class="text-muted">Data Evaluator MLCube: {{ benchmark.data_evaluator_mlcube }}</small></p>
<p class="card-text"><small class="text-muted">Approval Status: {{ benchmark.approval_status }}</small></p>
<p class="card-text"><small class="text-muted">Registered: {{ benchmark.is_registered }}</small></p>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
Loading
Loading