-
Notifications
You must be signed in to change notification settings - Fork 228
feat(nimbus): add rollout setup progress and issues modal #16239
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ | |
| NimbusExperiment, | ||
| NimbusFeatureConfig, | ||
| NimbusFeatureVersion, | ||
| NimbusRolloutPhase, | ||
| NimbusVersionedSchema, | ||
| ) | ||
| from experimenter.features.manifests.nimbus_fml_loader import NimbusFmlLoader | ||
|
|
@@ -1994,6 +1995,39 @@ def validate(self, data): | |
| return data | ||
|
|
||
|
|
||
| class NimbusRolloutReviewSerializer(NimbusReviewSerializer): | ||
| population_percent = serializers.DecimalField( | ||
| 7, | ||
| 4, | ||
| min_value=0.0, | ||
| max_value=100.0, | ||
| required=True, | ||
| error_messages={"min_value": NimbusConstants.ERROR_POPULATION_PERCENT_MIN}, | ||
| ) | ||
| total_enrolled_clients = serializers.IntegerField(required=False, min_value=0) | ||
| rollout_phases = serializers.PrimaryKeyRelatedField( | ||
| many=True, | ||
| required=False, | ||
| queryset=NimbusRolloutPhase.objects.all(), | ||
| ) | ||
|
|
||
| def validate_reference_branch(self, value): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we add a comment here why we are simply returning the value for the future purpose |
||
| return value | ||
|
|
||
| def validate_rollout_phases(self, value): | ||
| if self.instance and not self.instance.is_rollout: | ||
| return value | ||
|
|
||
| phases = list(value) | ||
| if not phases: | ||
| raise serializers.ValidationError(NimbusConstants.ERROR_ROLLOUT_NO_PHASES) | ||
| if not phases[0].population_percent: | ||
| raise serializers.ValidationError( | ||
| NimbusConstants.ERROR_ROLLOUT_FIRST_PHASE_ZERO | ||
| ) | ||
| return value | ||
|
|
||
|
|
||
| class LocalizationError(Exception): | ||
| """An error that occurs during localization substitution.""" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| from django.views.generic import DetailView | ||
| from django.views.generic.edit import UpdateView | ||
|
|
||
| from experimenter.experiments.api.v5.serializers import NimbusRolloutReviewSerializer | ||
| from experimenter.experiments.models import NimbusExperiment, Tag | ||
| from experimenter.nimbus_ui.filtersets import ( | ||
| TagSearchFilterSet, | ||
|
|
@@ -94,7 +95,86 @@ def post(self, request, *args, **kwargs): | |
| return super().post(request, *args, **kwargs) | ||
|
|
||
|
|
||
| class RolloutSetupProgressMixin: | ||
| SETUP_SECTIONS = { | ||
| "Overview": {*RolloutOverviewForm.Meta.fields}, | ||
| "Risks": {*RolloutRisksForm.Meta.fields}, | ||
| "Features": {*RolloutFeaturesForm.Meta.fields}, | ||
| "Audience": {*RolloutAudienceForm.Meta.fields}, | ||
| } | ||
|
|
||
| SECTION_CARD_IDS = { | ||
| "Overview": "overview", | ||
| "Risks": "risks", | ||
| "Features": "rollout-features", | ||
| "Audience": "audience", | ||
| } | ||
|
|
||
| NON_FIELD_ISSUE_CARDS = { | ||
| "rollout_phases": { | ||
| "section": "Rollout schedule", | ||
| "card_id": "schedule", | ||
| "label": "Rollout Schedule", | ||
| }, | ||
| } | ||
|
Comment on lines
+99
to
+119
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| def flatten_errors(self, messages): | ||
| if isinstance(messages, dict): | ||
| return [m for value in messages.values() for m in self.flatten_errors(value)] | ||
| if isinstance(messages, (list, tuple)): | ||
| return [m for value in messages for m in self.flatten_errors(value)] | ||
| return [str(messages)] | ||
|
|
||
| def get_context_data(self, **kwargs): | ||
| context = super().get_context_data(**kwargs) | ||
| field_errors = self.object.get_invalid_fields_errors( | ||
| serializer_class=NimbusRolloutReviewSerializer | ||
| ) | ||
| error_keys = set(field_errors) | ||
|
|
||
| tracked_fields = set() | ||
| for fields in self.SETUP_SECTIONS.values(): | ||
| tracked_fields.update(fields) | ||
| tracked_fields.update(self.NON_FIELD_ISSUE_CARDS) | ||
| tracked_fields.update(error_keys) | ||
|
|
||
| invalid_tracked = error_keys & tracked_fields | ||
| context["setup_completion_percent"] = round( | ||
| 100 * (len(tracked_fields) - len(invalid_tracked)) / len(tracked_fields) | ||
| ) | ||
|
|
||
|
Comment on lines
+135
to
+145
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
error_keys & tracked_fields then always equals tracked_fields = set() broken = error_keys & tracked_fields One decision for us to make first: should an unexpected error drag the progress bar down, or just show up in the issues list without affecting the percent? With the fixed denominator it does the latter. Right now test_setup_progress_untracked_issue_lowers_completion expects the former — so let's agree on the intended behavior and make the code + test match. My vote is the bar tracks only known setup fields, and unknown errors just appear as issues. |
||
| field_to_section = { | ||
| field: section | ||
| for section, fields in self.SETUP_SECTIONS.items() | ||
| for field in fields | ||
| } | ||
| issues = [] | ||
| for field, messages in field_errors.items(): | ||
| if field in self.NON_FIELD_ISSUE_CARDS: | ||
| meta = self.NON_FIELD_ISSUE_CARDS[field] | ||
| section = meta["section"] | ||
| card_id = meta["card_id"] | ||
| label = meta["label"] | ||
| else: | ||
| section = field_to_section.get(field, "Other") | ||
| card_id = self.SECTION_CARD_IDS.get(section) | ||
| label = field.replace("_", " ").title() | ||
| issues.append( | ||
| { | ||
| "section": section, | ||
| "card_id": card_id, | ||
| "label": label, | ||
| "messages": self.flatten_errors(messages), | ||
| } | ||
| ) | ||
|
|
||
| context["setup_issues"] = issues | ||
| context["setup_issues_count"] = len(issues) | ||
| return context | ||
|
|
||
|
|
||
| class NimbusRolloutDetailView( | ||
| RolloutSetupProgressMixin, | ||
| NimbusExperimentViewMixin, | ||
| CloneExperimentFormMixin, | ||
| DetailView, | ||
|
|
@@ -118,6 +198,7 @@ def get_context_data(self, **kwargs): | |
|
|
||
|
|
||
| class NewCardUpdateView( | ||
| RolloutSetupProgressMixin, | ||
| NimbusExperimentViewMixin, | ||
| RequestFormMixin, | ||
| UpdateRedirectViewMixin, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| {% block card %}{% endblock %} | ||
| {% include "new/common/sidebar_setup_refresh.html" %} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| <div id="sidebar-setup-issues-modal" | ||
| {% if oob %}hx-swap-oob="true"{% endif %}> | ||
| {% if setup_issues_count %} | ||
| <div class="modal fade" id="setupIssuesModal" tabindex="-1"> | ||
| <div class="modal-dialog modal-dialog-scrollable"> | ||
| <div class="modal-content"> | ||
| <div class="modal-header"> | ||
| <h1 class="modal-title fs-5 d-flex align-items-center gap-2">Rollout Issues Detected</h1> | ||
| <button type="button" class="btn-close" data-bs-dismiss="modal"></button> | ||
| </div> | ||
| <div class="modal-body bg-body-tertiary d-flex flex-column gap-2"> | ||
| {% for issue in setup_issues %} | ||
| <div class="rounded-2 p-3 small" | ||
| style="background-color: var(--bs-warning-bg-subtle); | ||
| border: 1px solid var(--bs-warning-border-subtle)"> | ||
| <button type="button" | ||
| class="rollout-card-toggle btn btn-link small p-0 w-100 d-flex align-items-center gap-2 text-black text-decoration-none collapsed" | ||
| data-bs-toggle="collapse" | ||
| data-bs-target="#setupIssue{{ forloop.counter }}"> | ||
| <i class="fa-solid fa-triangle-exclamation" | ||
| style="color: var(--bs-orange)"></i> | ||
| <span class="flex-grow-1 small text-start fw-semibold">{{ issue.label }}</span> | ||
| <i class="fa-solid fa-chevron-right chevron-icon" | ||
| style="font-size: 11px"></i> | ||
| </button> | ||
| <div class="collapse ms-4" id="setupIssue{{ forloop.counter }}"> | ||
| {% for message in issue.messages %}<div>{{ message }}</div>{% endfor %} | ||
| {% if issue.card_id %} | ||
| <button type="button" | ||
| class="btn btn-sm mt-2" | ||
| style="background-color: var(--bs-warning-border-subtle); | ||
| border: 1px solid var(--bs-warning-border-subtle)" | ||
| data-bs-dismiss="modal" | ||
| data-setup-issue-target="{{ issue.card_id }}"> | ||
| Go to {{ issue.section }} | ||
| <i class="fa-solid fa-arrow-right ms-1" style="font-size: 10px;"></i> | ||
| </button> | ||
| {% endif %} | ||
| </div> | ||
| </div> | ||
| {% endfor %} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| {% endif %} | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
min_value=0.0 is what we want for rollouts, but you're still passing the old "must be >= 0.0001" message — it can never fire