Skip to content

Commit a4a1abc

Browse files
committed
[web] Add delete action to A/B experiment detail page
Adds a Delete button to the experiment header. Clicking it opens a Bootstrap modal that requires the user to type "delete" before the "Permanently delete" button becomes active, preventing accidental deletions. On confirmation, POST /abtest/<id>/delete clears the experiment's run FKs, deletes the ABExperiment row, then deletes the associated ABRun rows (which cascade-delete their ABSample children via the existing ab_samples relationship). After deletion the user is redirected to the experiments list page. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> Pull Request: #223
1 parent cca130b commit a4a1abc

3 files changed

Lines changed: 72 additions & 0 deletions

File tree

lnt/server/ui/templates/v4_abtest.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
function selectAll(source) {
2525
$(source).closest("table").find("input:checkbox").prop("checked", source.checked);
2626
}
27+
function checkDeleteConfirm() {
28+
var val = document.getElementById('delete-confirm-input').value;
29+
var btn = document.getElementById('delete-confirm-btn');
30+
btn.disabled = (val !== 'delete');
31+
}
2732
</script>
2833
{% endblock %}
2934

@@ -56,6 +61,37 @@ <h3>
5661
</form>
5762
<small class="text-muted" style="margin-left:8px">Keep Forever &mdash; created {{ exp.created_time }}</small>
5863

64+
<a href="#deleteModal" role="button" class="btn btn-danger btn-small"
65+
style="margin-left:1em" data-toggle="modal">Delete</a>
66+
67+
{# ------------------------------------------------------------------ #}
68+
{# Delete confirmation modal #}
69+
{# ------------------------------------------------------------------ #}
70+
<div id="deleteModal" class="modal hide fade" tabindex="-1" role="dialog"
71+
aria-labelledby="deleteModalLabel" aria-hidden="true">
72+
<div class="modal-header">
73+
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
74+
<h3 id="deleteModalLabel">Delete experiment?</h3>
75+
</div>
76+
<div class="modal-body">
77+
<p>This will permanently delete experiment
78+
<strong>{{ exp.name or ('Experiment #' ~ exp.id) }}</strong>
79+
and all of its run data. This cannot be undone.</p>
80+
<p>Type <code>delete</code> to confirm:</p>
81+
<input id="delete-confirm-input" type="text" oninput="checkDeleteConfirm()"
82+
placeholder="delete" autocomplete="off" style="width:100%"/>
83+
</div>
84+
<div class="modal-footer">
85+
<button class="btn" data-dismiss="modal">Cancel</button>
86+
<form method="POST"
87+
action="{{ v4_url_for('.v4_abtest_delete', id=exp.id) }}"
88+
style="display:inline">
89+
<button id="delete-confirm-btn" type="submit"
90+
class="btn btn-danger" disabled>Permanently delete</button>
91+
</form>
92+
</div>
93+
</div>
94+
5995
<div style="margin-top:1em"></div>
6096

6197
{% if pending %}

lnt/server/ui/views.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2161,6 +2161,31 @@ def v4_abtest_pin(id):
21612161
return v4_redirect(v4_url_for('.v4_abtest', id=id))
21622162

21632163

2164+
@v4_route("/abtest/<int:id>/delete", methods=['POST'])
2165+
def v4_abtest_delete(id):
2166+
"""Permanently delete an A/B experiment and its associated runs/samples."""
2167+
session = request.session
2168+
ts = request.get_testsuite()
2169+
exp = session.query(ts.ABExperiment).filter_by(id=id).first()
2170+
if exp is None:
2171+
abort(404, "No A/B experiment with id %d." % id)
2172+
run_ids = [rid for rid in [exp.control_run_id, exp.variant_run_id]
2173+
if rid is not None]
2174+
# Clear the FKs first so the run rows can be deleted without a
2175+
# FK constraint violation, then delete the experiment itself.
2176+
exp.control_run_id = None
2177+
exp.variant_run_id = None
2178+
session.flush()
2179+
session.delete(exp)
2180+
session.flush()
2181+
for rid in run_ids:
2182+
run = session.query(ts.ABRun).filter_by(id=rid).first()
2183+
if run is not None:
2184+
session.delete(run) # cascades to ABSample via ab_samples
2185+
session.commit()
2186+
return v4_redirect(v4_url_for('.v4_abtests'))
2187+
2188+
21642189
@v4_route("/abtest/<int:id>/scurve")
21652190
def v4_abtest_scurve(id):
21662191
"""S-curve graph for selected benchmarks in an A/B experiment."""

tests/server/ui/v4/V4Pages.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,17 @@ def _post_run(exp_id, role, run_data):
864864
_post_run(pending_control, 'variant', _sample_run)
865865
check_html(client, '/db_default/v4/nts/abtest/%d' % pending_control)
866866

867+
# Delete: POST to delete route redirects to the list page.
868+
delete_id = _post_abtest({'name': 'to-be-deleted'})
869+
_post_run(delete_id, 'control', _sample_run)
870+
check_html(client, '/db_default/v4/nts/abtest/%d' % delete_id)
871+
r = client.post('/db_default/v4/nts/abtest/%d/delete' % delete_id)
872+
assert r.status_code in (301, 302), \
873+
"Delete returned %d, expected redirect" % r.status_code
874+
# Confirm the experiment is gone.
875+
check_code(client, '/db_default/v4/nts/abtest/%d' % delete_id,
876+
expected_code=HTTP_NOT_FOUND)
877+
867878

868879
if __name__ == '__main__':
869880
main()

0 commit comments

Comments
 (0)