diff --git a/.gitignore b/.gitignore index 0a915883..6e7e2975 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ dist/ # NPM server/frontend/node_modules server/frontend/dist + +# Git worktrees +.worktrees/ diff --git a/server/frontend/src/components/Buckets/HideBucketBtn.vue b/server/frontend/src/components/Buckets/HideBucketBtn.vue deleted file mode 100644 index 6acd3cd5..00000000 --- a/server/frontend/src/components/Buckets/HideBucketBtn.vue +++ /dev/null @@ -1,50 +0,0 @@ - - Mark triaged - - - diff --git a/server/frontend/src/components/Buckets/HideBucketBtnForm.vue b/server/frontend/src/components/Buckets/HideBucketBtnForm.vue deleted file mode 100644 index 9968bf92..00000000 --- a/server/frontend/src/components/Buckets/HideBucketBtnForm.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - - This will hide the bucket from the default view for the specified number - of weeks. - - - Incoming reports matching the signature will still be put in this bucket. - - - - Weeks - - - - - - - - - diff --git a/server/frontend/src/components/Buckets/List.vue b/server/frontend/src/components/Buckets/List.vue index c62b941b..ed30afee 100644 --- a/server/frontend/src/components/Buckets/List.vue +++ b/server/frontend/src/components/Buckets/List.vue @@ -18,6 +18,7 @@ @@ -293,7 +294,7 @@ export default { pageSize: 100, queryError: "", queryStr: JSON.stringify( - { op: "AND", bug__isnull: true, hide_until__isnull: true }, + { op: "AND", bug__isnull: true, triage_status__isnull: true }, null, 2, ), @@ -325,7 +326,7 @@ export default { return !this.queryStr.includes('"bug__isnull": true'); }, showHidden() { - return !this.queryStr.includes('"hide_until__isnull": true'); + return !this.queryStr.includes('"triage_status__isnull": true'); }, }, created() { @@ -385,7 +386,7 @@ export default { if (this.showHidden) { this.queryStr = JSON.stringify( Object.assign( - { hide_until__isnull: true }, + { triage_status__isnull: true }, JSON.parse(this.queryStr), ), null, @@ -393,7 +394,7 @@ export default { ); } else { const query = JSON.parse(this.queryStr); - delete query["hide_until__isnull"]; + delete query["triage_status__isnull"]; this.queryStr = JSON.stringify(query, null, 2); } this.fetch(); diff --git a/server/frontend/src/components/Buckets/TriageBucketDropdown.vue b/server/frontend/src/components/Buckets/TriageBucketDropdown.vue new file mode 100644 index 00000000..18bc36e1 --- /dev/null +++ b/server/frontend/src/components/Buckets/TriageBucketDropdown.vue @@ -0,0 +1,214 @@ + + + + {{ currentStatus ? "Change triage status" : "Mark triaged" }} + + + + Select status + + + + {{ choice.label }} + ✓ + + + + + + Unmark triaged + + + + + + + + + + + + diff --git a/server/frontend/src/components/Buckets/View.vue b/server/frontend/src/components/Buckets/View.vue index e86a8b8a..97336941 100644 --- a/server/frontend/src/components/Buckets/View.vue +++ b/server/frontend/src/components/Buckets/View.vue @@ -32,19 +32,22 @@ No bug associated. - Marked triaged until {{ formatDate(bucket.hide_until) }}.Marked triaged as: + + {{ triageStatusDisplay }} + + on {{ formatDate(triagedAt) }}. - - Unmark triaged @@ -227,9 +230,9 @@ import { assignExternalBug, date, errorParser, - hideBucketUntil, jsonPretty, parseHash, + updateBucketTriageStatus, } from "../../helpers"; import { etpStrictReportDescription, @@ -241,7 +244,7 @@ import * as api from "../../api"; import PageNav from "../PageNav.vue"; import ActivityGraph from "../ActivityGraph.vue"; import AssignBtn from "./AssignBtn.vue"; -import HideBucketBtn from "./HideBucketBtn.vue"; +import TriageBucketDropdown from "./TriageBucketDropdown.vue"; import ReportPreviewRow from "./ReportPreviewRow.vue"; const pageSize = 50; @@ -251,7 +254,7 @@ export default { activitygraph: ActivityGraph, assignbutton: AssignBtn, ClipLoader: LoadingSpinner, - hidebucketbutton: HideBucketBtn, + TriageBucketDropdown, PageNav: PageNav, ReportPreviewRow: ReportPreviewRow, }, @@ -304,6 +307,9 @@ export default { reports: null, sortKeys: [...defaultSortKeys], totalPages: 1, + triageStatus: this.bucket.triage_status, + triageStatusDisplay: this.bucket.triage_status_display, + triagedAt: this.bucket.triaged_at, }; }, computed: { @@ -401,15 +407,6 @@ export default { submitWatchForm() { this.$refs.bucketWatchForm.submit(); }, - unhide() { - hideBucketUntil(this.bucket.id, null) - .then((data) => { - window.location.href = data.url; - }) - .catch((err) => { - swal("Oops", errorParser(err), "error"); - }); - }, unlink() { swal({ title: "Unlink bug", @@ -532,6 +529,19 @@ export default { searchParams.append("dependson", "tp-breakage"); openPrefilledBugzillaBug(searchParams); }, + async handleTriageStatusUpdate(newStatus) { + try { + const { bucket } = await updateBucketTriageStatus( + this.bucket.id, + newStatus, + ); + this.triageStatus = bucket.triage_status; + this.triageStatusDisplay = bucket.triage_status_display; + this.triagedAt = bucket.triaged_at; + } catch (err) { + swal("Oops", errorParser(err), "error"); + } + }, }, watch: { currentPage() { diff --git a/server/frontend/src/helpers.js b/server/frontend/src/helpers.js index 63e63668..0733b0e8 100644 --- a/server/frontend/src/helpers.js +++ b/server/frontend/src/helpers.js @@ -89,9 +89,9 @@ export const assignExternalBug = (bucketId, bugId, providerId) => { } }; -export const hideBucketUntil = (bucketId, dateObj) => { +export const updateBucketTriageStatus = (bucketId, triageStatus) => { const payload = { - hide_until: dateObj ? dateObj.toISOString() : null, + triage_status: triageStatus, }; try { diff --git a/server/frontend/tests/buckets_list.test.js b/server/frontend/tests/buckets_list.test.js index 2544f16f..c4aa9bd7 100644 --- a/server/frontend/tests/buckets_list.test.js +++ b/server/frontend/tests/buckets_list.test.js @@ -16,7 +16,7 @@ afterEach(jest.resetAllMocks); const defaultQueryStr = `{ "op": "AND", "bug__isnull": true, - "hide_until__isnull": true + "triage_status__isnull": true }`; test("bucket list has no buckets", async () => { diff --git a/server/reportmanager/cron.py b/server/reportmanager/cron.py index b5a456eb..c9011845 100644 --- a/server/reportmanager/cron.py +++ b/server/reportmanager/cron.py @@ -58,14 +58,6 @@ def update_report_stats(): ReportHit.objects.filter(last_update__lt=old_cutoff).delete() -@app.task(ignore_result=True) -def unhide_buckets(): - from .models import Bucket - - now = timezone.now() - Bucket.objects.filter(hide_until__lte=now).update(hide_until=None) - - @app.task(ignore_result=True) def bug_update_status(): call_command("bug_update_status") diff --git a/server/reportmanager/migrations/0018_add_triage_status.py b/server/reportmanager/migrations/0018_add_triage_status.py new file mode 100644 index 00000000..bda9585f --- /dev/null +++ b/server/reportmanager/migrations/0018_add_triage_status.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.4 on 2026-04-29 17:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reportmanager', '0017_reportentry_country'), + ] + + operations = [ + migrations.AddField( + model_name='bucket', + name='triage_status', + field=models.CharField(choices=[('worksforme', 'Works For Me'), ('cant_test', "Can't Test"), ('incomplete', 'Incomplete'), ('invalid', 'Invalid'), ('non_compat', 'Non-Compat')], max_length=20, null=True), + ), + migrations.AddField( + model_name='bucket', + name='triaged_at', + field=models.DateTimeField(null=True), + ), + ] diff --git a/server/reportmanager/migrations/0019_migrate_hide_until_to_triage_status.py b/server/reportmanager/migrations/0019_migrate_hide_until_to_triage_status.py new file mode 100644 index 00000000..48fa73e7 --- /dev/null +++ b/server/reportmanager/migrations/0019_migrate_hide_until_to_triage_status.py @@ -0,0 +1,41 @@ +# Generated by Django 6.0.4 on 2026-04-24 17:02 + +from datetime import timedelta + +from django.db import migrations +from django.db.models import ExpressionWrapper, F +from django.db.models.fields import DateTimeField + + +def migrate_triage_data(apps, schema_editor): + """Migrate hide_until data to triage_status field.""" + Bucket = apps.get_model('reportmanager', 'Bucket') + + # All currently triaged buckets (hide_until is not NULL) → 'worksforme' + # triaged_at is set to hide_until - 4 weeks to approximate when triage happened + triaged_count = Bucket.objects.filter(hide_until__isnull=False).update( + triage_status='worksforme', + triaged_at=ExpressionWrapper( + F('hide_until') - timedelta(weeks=4), + output_field=DateTimeField(), + ), + ) + + print(f"Migrated {triaged_count} triaged buckets to 'worksforme' status") + + +def reverse_migrate(apps, schema_editor): + """Reverse migration - clear triage_status.""" + Bucket = apps.get_model('reportmanager', 'Bucket') + Bucket.objects.all().update(triage_status=None, triaged_at=None) + + +class Migration(migrations.Migration): + + dependencies = [ + ('reportmanager', '0018_add_triage_status'), + ] + + operations = [ + migrations.RunPython(migrate_triage_data, reverse_migrate), + ] diff --git a/server/reportmanager/migrations/0020_remove_hide_until.py b/server/reportmanager/migrations/0020_remove_hide_until.py new file mode 100644 index 00000000..16c62d31 --- /dev/null +++ b/server/reportmanager/migrations/0020_remove_hide_until.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.4 on 2026-04-24 17:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reportmanager', '0019_migrate_hide_until_to_triage_status'), + ] + + operations = [ + migrations.RemoveField( + model_name='bucket', + name='hide_until', + ), + ] diff --git a/server/reportmanager/models.py b/server/reportmanager/models.py index 747f18bc..8864ded1 100644 --- a/server/reportmanager/models.py +++ b/server/reportmanager/models.py @@ -52,6 +52,13 @@ class BreakageCategory(models.Model): class Bucket(models.Model): + class TriageStatus(models.TextChoices): + WORKS_FOR_ME = "worksforme", "Works For Me" + CANT_TEST = "cant_test", "Can't Test" + INCOMPLETE = "incomplete", "Incomplete" + INVALID = "invalid", "Invalid" + NON_COMPAT = "non_compat", "Non-Compat" + id: models.AutoField = models.AutoField(primary_key=True) bug: models.ForeignKey = models.ForeignKey( "Bug", null=True, on_delete=models.deletion.CASCADE @@ -63,8 +70,10 @@ class Bucket(models.Model): # store the domain outside the signature only if the signature includes # a non-regex domain symptom and no other symptoms (for quick exclusion) domain: models.CharField = models.CharField(max_length=255, null=True) - # bucket can be hidden from view until given date, without being logged - hide_until: models.DateTimeField = models.DateTimeField(blank=True, null=True) + triage_status: models.CharField = models.CharField( + max_length=20, null=True, choices=TriageStatus.choices + ) + triaged_at: models.DateTimeField = models.DateTimeField(null=True) # higher priority = earlier match priority: models.IntegerField = models.IntegerField( default=0, validators=(MinValueValidator(-2), MaxValueValidator(2)) diff --git a/server/reportmanager/serializers.py b/server/reportmanager/serializers.py index e2390de7..b86d6f34 100644 --- a/server/reportmanager/serializers.py +++ b/server/reportmanager/serializers.py @@ -42,6 +42,9 @@ class BucketSerializer(serializers.ModelSerializer): ) size = serializers.IntegerField(write_only=True, required=False) priority_score = serializers.FloatField(write_only=True, required=False) + triage_status_display = serializers.CharField( + source="get_triage_status_display", read_only=True + ) class Meta: model = Bucket @@ -51,19 +54,22 @@ class Meta: "color", "description", "domain", - "hide_until", "id", "latest_entry_id", "latest_report", "priority", "signature", "size", + "triage_status", + "triage_status_display", + "triaged_at", "reassign_in_progress", "priority_score", ] ordering = ("-id",) read_only_fields = [ "id", + "triaged_at", ] def to_internal_value(self, data): @@ -74,10 +80,14 @@ def to_internal_value(self, data): def to_representation(self, obj): serialized = super().to_representation(obj) - serialized["size"] = obj.size + serialized["size"] = getattr(obj, "size", None) serialized["latest_entry_id"] = getattr(obj, "latest_entry_id", None) serialized["latest_report"] = getattr(obj, "latest_report", None) serialized["priority_score"] = getattr(obj, "priority_score", None) + serialized["triage_status_choices"] = [ + {"value": choice[0], "label": choice[1]} + for choice in Bucket.TriageStatus.choices + ] return serialized diff --git a/server/reportmanager/templates/layouts/layout_base.html b/server/reportmanager/templates/layouts/layout_base.html index a929670f..8bfbb405 100644 --- a/server/reportmanager/templates/layouts/layout_base.html +++ b/server/reportmanager/templates/layouts/layout_base.html @@ -32,46 +32,6 @@ - - {% endblock js%} {% if debug %}
- This will hide the bucket from the default view for the specified number - of weeks. -
- Incoming reports matching the signature will still be put in this bucket. -