Spaces:
Sleeping
Sleeping
File size: 6,624 Bytes
6973475 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | {% extends "base.html" %}
{% set active_page = "models" %}
{% block title %}Model Registry{% endblock %}
{% block page_title %}<i class="fa-solid fa-box-archive" style="color:var(--warning)"></i> Model Registry{% endblock %}
{% block content %}
<div class="page-title">Model Registry</div>
<div class="page-sub">Track model versions and manage stage transitions: None β Staging β Production β Archived</div>
{% if models %}
<div style="display:flex;flex-direction:column;gap:16px">
{% for model in models %}
<div class="card">
<div class="flex-between" style="margin-bottom:14px">
<div>
<div style="font-weight:700;font-size:1rem">{{ model.name }}</div>
<div style="font-size:.82rem;color:var(--text-secondary);margin-top:2px">{{ model.description }}</div>
</div>
<span class="badge stage-{{ model.latest_stage | lower }}">
{% if model.latest_stage == 'Production' %}<i class="fa-solid fa-rocket"></i>
{% elif model.latest_stage == 'Staging' %}<i class="fa-solid fa-flask"></i>
{% elif model.latest_stage == 'Archived' %}<i class="fa-solid fa-archive"></i>
{% else %}<i class="fa-solid fa-box"></i>{% endif %}
{{ model.latest_stage }}
</span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr><th>Version</th><th>Stage</th><th>Run ID</th><th>Key Metrics</th><th>Registered</th><th>Actions</th></tr>
</thead>
<tbody>
{% for v in model.versions %}
<tr>
<td><span class="badge badge-muted">v{{ v.version }}</span></td>
<td>
<span class="badge stage-{{ v.stage | lower }}">
{{ v.stage }}
</span>
</td>
<td><code style="font-size:.8rem;color:var(--accent-light)">{{ v.run_id }}</code></td>
<td>
{% if v.metrics %}
{% for k, val in v.metrics.items() %}
<span style="font-size:.8rem;margin-right:8px">
<span style="color:var(--text-muted)">{{ k }}:</span>
<strong>{{ val }}</strong>
</span>
{% endfor %}
{% else %}β{% endif %}
</td>
<td style="font-size:.8rem;color:var(--text-muted)">{{ v.created_at }}</td>
<td>
<div style="display:flex;gap:6px;flex-wrap:wrap">
{% if v.stage != 'Staging' and v.stage != 'Production' %}
<button class="btn btn-ghost btn-sm"
onclick="transitionStage('{{ model.name }}','{{ v.version }}','Staging', this)">
β Staging
</button>
{% endif %}
{% if v.stage == 'Staging' %}
<button class="btn btn-success btn-sm"
onclick="transitionStage('{{ model.name }}','{{ v.version }}','Production', this)">
<i class="fa-solid fa-rocket"></i> Promote
</button>
{% endif %}
{% if v.stage == 'Production' %}
<button class="btn btn-warning btn-sm"
onclick="transitionStage('{{ model.name }}','{{ v.version }}','Archived', this)">
Archive
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="card">
<div class="empty-state">
<div class="empty-state-icon">π¦</div>
<div class="empty-state-title">No registered models yet</div>
<div style="margin-bottom:16px;max-width:400px;margin-left:auto;margin-right:auto">
Run the <strong>Training Pipeline</strong> or use the AutoML sweep,
then register the best model from the Experiments page.
</div>
<a href="/pipeline" class="btn btn-primary"><i class="fa-solid fa-diagram-project"></i> Go to Pipelines</a>
</div>
</div>
{% endif %}
<!-- Register modal -->
<div class="modal-overlay" id="register-modal">
<div class="modal">
<div class="modal-header">
<div class="modal-title"><i class="fa-solid fa-plus" style="color:var(--accent)"></i> Register Model</div>
<button class="modal-close" onclick="document.getElementById('register-modal').classList.remove('open')"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="form-group">
<label class="form-label">MLflow Run ID</label>
<input type="text" class="form-control" id="reg-run-id" placeholder="e.g. abc12345β¦">
</div>
<div class="form-group">
<label class="form-label">Model Name</label>
<input type="text" class="form-control" id="reg-name" placeholder="e.g. iris-classifier">
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="document.getElementById('register-modal').classList.remove('open')">Cancel</button>
<button class="btn btn-primary" onclick="registerModel()"><i class="fa-solid fa-box-archive"></i> Register</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function transitionStage(name, version, stage, btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spinner" style="width:12px;height:12px;border-width:1.5px"></span>';
try {
const res = await fetch(`/api/models/${encodeURIComponent(name)}/${version}/stage`, {
method: 'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ stage }),
});
const data = await res.json();
if (data.error) { showToast(data.error, 'error'); btn.disabled = false; return; }
showToast(`${name} v${version} β ${stage}`, 'success');
setTimeout(() => location.reload(), 900);
} catch(e) { showToast('Request failed', 'error'); btn.disabled = false; }
}
async function registerModel() {
const runId = document.getElementById('reg-run-id').value.trim();
const name = document.getElementById('reg-name').value.trim();
if (!runId || !name) { showToast('Run ID and name are required', 'error'); return; }
try {
const res = await fetch('/api/models/register', {
method: 'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ run_id: runId, name }),
});
const data = await res.json();
if (data.error) { showToast(data.error, 'error'); return; }
showToast(`Registered ${data.name} v${data.version}`, 'success');
setTimeout(() => location.reload(), 900);
} catch(e) { showToast('Request failed', 'error'); }
}
</script>
{% endblock %}
|