Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions experimenter/experimenter/nimbus_ui/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,22 @@ class NimbusUIConstants:
"phase again, or cancel and add a new phase in the rollout schedule."
)

TOAST_SAVED = "toast-saved"
TOAST_SUBSCRIBED = "toast-subscribed"
TOAST_UNSUBSCRIBED = "toast-unsubscribed"
TOAST_SLACK_ENABLED = "toast-slack-enabled"
TOAST_SLACK_DISABLED = "toast-slack-disabled"
TOAST_EDIT_CANCELLED = "toast-edit-cancelled"

TOASTS = {
TOAST_SAVED: "Changes saved",
TOAST_SUBSCRIBED: "Subscribed to this experiment",
TOAST_UNSUBSCRIBED: "Unsubscribed from this experiment",
TOAST_SLACK_ENABLED: "Review Slack notifications enabled",
TOAST_SLACK_DISABLED: "Review Slack notifications disabled",
TOAST_EDIT_CANCELLED: "Edit cancelled",
}

class RolloutPhaseStatus:
NOT_STARTED = "not_started"
IN_PROGRESS = "in_progress"
Expand Down
33 changes: 27 additions & 6 deletions experimenter/experimenter/nimbus_ui/new/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json

from django import forms
from django.conf import settings
from django.http import HttpResponse, HttpResponseRedirect
Expand All @@ -8,6 +10,7 @@
from experimenter.experiments.api.v5.serializers import NimbusRolloutReviewSerializer
from experimenter.experiments.constants import EXTERNAL_URLS, RISK_QUESTIONS
from experimenter.experiments.models import NimbusExperiment, Tag
from experimenter.nimbus_ui.constants import NimbusUIConstants
from experimenter.nimbus_ui.filtersets import (
TagSearchFilterSet,
UserSearchFilterSet,
Expand Down Expand Up @@ -55,6 +58,11 @@
)


def trigger_toast(response, toast_id):
response.headers["HX-Trigger"] = json.dumps({"showToast": {"id": toast_id}})
return response


class CloneExperimentFormMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
Expand Down Expand Up @@ -316,6 +324,7 @@ class NewCardUpdateView(
UpdateView,
):
display_template = None
save_toast_id = NimbusUIConstants.TOAST_SAVED

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
Expand All @@ -331,10 +340,13 @@ def form_valid(self, form):

def render_valid_response(self):
context = self.get_context_data()
return self.response_class(
request=self.request,
template=self.display_template,
context=context,
return trigger_toast(
self.response_class(
request=self.request,
template=self.display_template,
context=context,
),
self.save_toast_id,
)


Expand Down Expand Up @@ -730,14 +742,18 @@ class NewSubscribeView(NimbusExperimentViewMixin, RequestFormMixin, UpdateView):
model = NimbusExperiment
form_class = SubscribeForm
template_name = "new/common/subscribe_bell.html"
toast_id = NimbusUIConstants.TOAST_SUBSCRIBED

def form_valid(self, form):
self.object = form.save()
return self.render_to_response(self.get_context_data())
return trigger_toast(
self.render_to_response(self.get_context_data()), self.toast_id
)


class NewUnsubscribeView(NewSubscribeView):
form_class = UnsubscribeForm
toast_id = NimbusUIConstants.TOAST_UNSUBSCRIBED


class NewCloneView(NimbusExperimentViewMixin, RequestFormMixin, UpdateView):
Expand Down Expand Up @@ -784,4 +800,9 @@ class NewToggleReviewSlackNotificationsView(

def form_valid(self, form):
self.object = form.save()
return self.render_to_response(self.get_context_data())
toast_id = (
NimbusUIConstants.TOAST_SLACK_ENABLED
if self.object.enable_review_slack_notifications
else NimbusUIConstants.TOAST_SLACK_DISABLED
)
return trigger_toast(self.render_to_response(self.get_context_data()), toast_id)
13 changes: 13 additions & 0 deletions experimenter/experimenter/nimbus_ui/static/css/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,19 @@
cursor: not-allowed;
}

.card-edit-form .card-edit-spinner {
display: none;
}

.card-edit-form.htmx-request {
opacity: 0.55;
pointer-events: none;

.card-edit-spinner {
display: inline-block;
}
}

.rollout-page {
.cm-editor .cm-scroller {
font-family: var(--bs-font-monospace);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const showToast = (toastId) => {
const toastEl = document.getElementById(toastId);
if (toastEl) {
window.bootstrap?.Toast.getOrCreateInstance(toastEl).show();
}
};

const syncCardEditActions = () => {
document.querySelectorAll(".rollout-card").forEach((card) => {
const editing = card.querySelector(".card-edit-form") !== null;
card
.querySelectorAll("[data-card-action='edit']")
.forEach((button) => button.classList.toggle("d-none", editing));
card
.querySelectorAll("[data-card-action='cancel']")
.forEach((button) => button.classList.toggle("d-none", !editing));
});
};

document.addEventListener("showToast", (event) => {
showToast(event.detail?.id);
});

document.addEventListener("click", (event) => {
const trigger = event.target.closest?.("[data-toast-id]");
if (trigger) {
showToast(trigger.dataset.toastId);
}
});

let pendingCardId = null;

document.addEventListener("htmx:beforeRequest", (event) => {
const card = event.detail.elt.closest?.(".rollout-card");
pendingCardId = card ? card.id : null;
});

document.addEventListener("htmx:afterSwap", syncCardEditActions);

document.addEventListener("htmx:afterSettle", () => {
const cardId = pendingCardId;
pendingCardId = null;
if (!cardId) {
return;
}
const card = document.getElementById(cardId);
if (card) {
card.scrollIntoView({ block: "nearest", behavior: "smooth" });
}
});

document.addEventListener("DOMContentLoaded", syncCardEditActions);
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = {
new_edit_branches: "./js/new/edit_branches.js",
new_tooltips: "./js/new/tooltips.js",
new_htmx_failure: "./js/new/htmx_failure.js",
new_rollout_cards: "./js/new/rollout_cards.js",
experiment_detail: "./js/experiment_detail.js",
branch_detail: "./js/branch_detail.js",
features_page: "./js/features_page.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<script src="{% static 'nimbus_ui/app.bundle.js' %}"></script>
<script src="{% static 'nimbus_ui/new_tooltips.bundle.js' %}"></script>
<script src="{% static 'nimbus_ui/new_htmx_failure.bundle.js' %}"></script>
<script src="{% static 'nimbus_ui/new_rollout_cards.bundle.js' %}"></script>
{% block extra_head %}{% endblock %}
</head>
<body class="m-0 p-0 rollout-page bg-body-tertiary"
Expand Down Expand Up @@ -136,6 +137,18 @@ <h1 id="experiment-header-name" class="mb-0 fw-semibold fs-4 text-body">{{ exper
JSON copied to clipboard!
</div>
</div>
{% for toast_id, toast_message in NimbusUIConstants.TOASTS.items %}
<div id="{{ toast_id }}"
class="toast hide text-bg-primary mb-1"
role="alert"
aria-live="assertive"
aria-atomic="true">
<div class="toast-body">
<i class="fa-regular fa-circle-check"></i>
{{ toast_message }}
</div>
</div>
{% endfor %}
</div>
</div>
<template id="template-toast">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% with 'nimbus-ui-new-update-'|add:card_id as url_name %}
<button class="btn p-0 border-0 text-secondary"
type="button"
data-card-action="edit"
aria-label="Edit {{ card_id }}"
hx-get="{% url url_name experiment.slug %}"
hx-target="#rollout-{{ card_id }}-body"
hx-swap="outerHTML">
<i class="fa-regular fa-pen-to-square"></i>
</button>
{% endwith %}
{% include "new/common/card_cancel_button.html" with icon_only=True %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<button type="button"
class="{% if icon_only %}btn p-0 border-0 text-secondary d-none{% else %}btn btn-outline-secondary btn-sm{% endif %}"
{% if icon_only %}data-card-action="cancel" aria-label="Cancel editing {{ card_id }}"{% endif %}
data-toast-id="{{ NimbusUIConstants.TOAST_EDIT_CANCELLED }}"
{% if card_id == "overview" %} hx-post="{% url 'nimbus-ui-new-cancel-overview' experiment.slug %}" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' {% else %} hx-get="{% url 'new-nimbus-ui-rollout-detail' experiment.slug %}" {% endif %}
hx-select="#rollout-card-{{ card_id }}"
hx-target="#rollout-card-{{ card_id }}"
hx-swap="outerHTML">
{% if icon_only %}
<i class="fa-solid fa-xmark"></i>
{% else %}
Cancel
{% endif %}
</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="d-flex align-items-center gap-2">
<button type="submit"
id="rollout-{{ card_id }}-save"
{% if submit_name %}name="{{ submit_name }}" value="True"{% endif %}
class="btn btn-primary btn-sm d-inline-flex align-items-center gap-2">
<span class="spinner-border spinner-border-sm card-edit-spinner"
role="status"
aria-hidden="true"></span>
Save
</button>
{% include "new/common/card_cancel_button.html" %}

</div>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
hx-trigger="change"
hx-target="#slack-notifications-toggle-form"
hx-swap="outerHTML"
hx-sync="this:drop"
class="d-flex align-items-center">
{% csrf_token %}
<div class="form-check form-switch d-flex align-items-center m-0">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<form id="subscribe-bell"
hx-post="{% url 'nimbus-ui-new-unsubscribe' experiment.slug %}"
hx-swap="outerHTML"
hx-sync="this:drop"
class="d-inline">
{% csrf_token %}
<button type="submit"
Expand All @@ -16,6 +17,7 @@
<form id="subscribe-bell"
hx-post="{% url 'nimbus-ui-new-subscribe' experiment.slug %}"
hx-swap="outerHTML"
hx-sync="this:drop"
class="d-inline">
{% csrf_token %}
<button type="submit"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
<form hx-post="{% url 'nimbus-ui-new-update-audience' experiment.slug %}"
hx-target="#rollout-audience-body"
hx-swap="outerHTML"
class="d-flex flex-column gap-4">
hx-disabled-elt="#rollout-audience-save"
class="card-edit-form d-flex flex-column gap-4">
{% csrf_token %}
<div class="row g-3">
<div class="col">
Expand Down Expand Up @@ -162,15 +163,8 @@
</div>
</div>
{# Action buttons #}
<div class="d-flex align-items-center gap-2">
<button type="submit" name="save" value="True" class="btn btn-primary btn-sm">Save</button>
<button type="button"
class="btn btn-outline-secondary btn-sm"
hx-get="{{ cancel_url }}"
hx-select="#rollout-card-audience"
hx-target="#rollout-card-audience"
hx-swap="outerHTML">Cancel</button>
</div>
{% include "new/common/card_form_actions.html" with card_id="audience" submit_name="save" %}

</form>
</div>
{% block extrascripts %}
Expand Down
Loading
Loading