-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Innovation sprint/autotriage/report issue #7337
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: innovation/autofill-triage
Are you sure you want to change the base?
Changes from all commits
9ef0379
ac09eb1
278b6eb
d51a510
c71eaaf
8d068a2
c7ec8cc
a0e75d3
dcb10b7
a53f70e
32fdf01
c7f011a
c4f8f24
616edc2
0d9ea84
c37225e
632c59f
c09ae65
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 |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| #nullable enable | ||
|
|
||
| using Bit.Admin.Enums; | ||
| using Bit.Admin.Models; | ||
| using Bit.Admin.Utilities; | ||
| using Bit.Core.Autofill.Repositories; | ||
| using Microsoft.AspNetCore.Authorization; | ||
| using Microsoft.AspNetCore.Mvc; | ||
|
|
||
| namespace Bit.Admin.Controllers; | ||
|
|
||
| [Authorize] | ||
| public class AutofillTriageController : Controller | ||
| { | ||
| private readonly IAutofillTriageReportRepository _repo; | ||
|
|
||
| public AutofillTriageController(IAutofillTriageReportRepository repo) | ||
| => _repo = repo; | ||
|
|
||
| [RequirePermission(Permission.User_List_View)] | ||
| public async Task<IActionResult> Index(int page = 1, int count = 25) | ||
| { | ||
| var skip = (page - 1) * count; | ||
|
kdenney marked this conversation as resolved.
kdenney marked this conversation as resolved.
kdenney marked this conversation as resolved.
|
||
| var reports = await _repo.GetActiveAsync(skip, count); | ||
| var model = new AutofillTriageModel | ||
| { | ||
| Items = reports.ToList(), | ||
| Page = page, | ||
| Count = count, | ||
| }; | ||
| return View(model); | ||
| } | ||
|
|
||
| [RequirePermission(Permission.User_List_View)] | ||
| public async Task<IActionResult> Details(Guid id) | ||
| { | ||
| var report = await _repo.GetByIdAsync(id); | ||
| if (report is null) | ||
| { | ||
| return NotFound(); | ||
| } | ||
|
|
||
| return View(report); | ||
| } | ||
|
|
||
| [HttpPost] | ||
| [ValidateAntiForgeryToken] | ||
| [RequirePermission(Permission.User_List_View)] | ||
| public async Task<IActionResult> Archive(Guid id) | ||
| { | ||
|
kdenney marked this conversation as resolved.
|
||
| await _repo.ArchiveAsync(id); | ||
| return RedirectToAction(nameof(Index)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| namespace Bit.Admin.Models; | ||
|
|
||
| public class AutofillTriageFieldResultModel | ||
| { | ||
| public string? HtmlId { get; set; } | ||
| public string? HtmlName { get; set; } | ||
| public string? HtmlType { get; set; } | ||
| public string? Placeholder { get; set; } | ||
| public string? AriaLabel { get; set; } | ||
| public string? Autocomplete { get; set; } | ||
| public string? FormIndex { get; set; } | ||
| public bool Eligible { get; set; } | ||
| public string? QualifiedAs { get; set; } | ||
| public List<AutofillTriageConditionResultModel> Conditions { get; set; } = []; | ||
| } | ||
|
|
||
| public class AutofillTriageConditionResultModel | ||
| { | ||
| public string? Description { get; set; } | ||
| public bool Passed { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| using Bit.Core.Autofill.Entities; | ||
|
|
||
| namespace Bit.Admin.Models; | ||
|
|
||
| public class AutofillTriageModel : PagedModel<AutofillTriageReport> | ||
| { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| @using System.Net | ||
| @using System.Text.Json | ||
| @using Bit.Admin.Models | ||
| @model Bit.Core.Autofill.Entities.AutofillTriageReport | ||
| @{ | ||
| ViewData["Title"] = "Autofill Triage Report"; | ||
| var fields = new List<AutofillTriageFieldResultModel>(); | ||
| string? parseError = null; | ||
| try | ||
| { | ||
| fields = JsonSerializer.Deserialize<List<AutofillTriageFieldResultModel>>(Model.ReportData, | ||
| new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? []; | ||
| } | ||
| catch | ||
| { | ||
| parseError = "Could not parse report data. The stored JSON may be malformed."; | ||
| } | ||
| } | ||
|
|
||
| <h1>Autofill Triage Report</h1> | ||
| <a asp-action="Index" class="btn btn-secondary mb-3">← Back to list</a> | ||
|
|
||
| <div class="card mb-4"> | ||
| <div class="card-body"> | ||
| <dl class="row"> | ||
| <dt class="col-sm-3">Created At</dt> | ||
| <dd class="col-sm-9">@Model.CreationDate.ToString("f")</dd> | ||
|
|
||
| <dt class="col-sm-3">Page URL</dt> | ||
| <dd class="col-sm-9"> | ||
| @{ var decodedPageUrl = WebUtility.HtmlDecode(Model.PageUrl); } | ||
| <a href="@decodedPageUrl" target="_blank" rel="noreferrer noopener">@decodedPageUrl</a> | ||
| </dd> | ||
|
|
||
| @if (Model.TargetElementRef != null) | ||
| { | ||
| <dt class="col-sm-3">Targeted Field</dt> | ||
| <dd class="col-sm-9"> | ||
| <code class="text-danger">@WebUtility.HtmlDecode(Model.TargetElementRef)</code> | ||
| <small class="text-muted">(field the user right-clicked)</small> | ||
| </dd> | ||
| } | ||
|
|
||
| <dt class="col-sm-3">Extension Version</dt> | ||
| <dd class="col-sm-9">@Model.ExtensionVersion</dd> | ||
|
|
||
| <dt class="col-sm-3">User Message</dt> | ||
| <dd class="col-sm-9">@(Model.UserMessage != null ? WebUtility.HtmlDecode(Model.UserMessage) : "(none)")</dd> | ||
| </dl> | ||
| <div class="mt-3 pt-3 border-top"> | ||
| <form method="post" asp-action="Archive" asp-route-id="@Model.Id" style="display:inline"> | ||
| <button type="submit" class="btn btn-outline-danger" | ||
| onclick="return confirm('Archive this report?')"> | ||
| Archive | ||
| </button> | ||
| @Html.AntiForgeryToken() | ||
| </form> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| @if (parseError != null) | ||
| { | ||
| <div class="alert alert-warning">@parseError</div> | ||
| } | ||
|
|
||
| <h2>Field Analysis (@fields.Count fields)</h2> | ||
|
|
||
| @for (var i = 0; i < fields.Count; i++) | ||
| { | ||
| var field = fields[i]; | ||
| var fieldLabel = field.HtmlId ?? field.HtmlName ?? field.Placeholder ?? "(unnamed)"; | ||
| var collapseId = $"field-{i}"; | ||
| var isTarget = field.HtmlId == Model.TargetElementRef || field.HtmlName == Model.TargetElementRef; | ||
| <div class="card mb-3 @(isTarget ? "border-primary" : "")"> | ||
| <div class="card-header d-flex justify-content-between align-items-center" | ||
| data-bs-toggle="collapse" data-bs-target="#@collapseId" | ||
| role="button" aria-expanded="false" aria-controls="@collapseId" | ||
| style="cursor: pointer; user-select: none;"> | ||
|
Check warning on line 79 in src/Admin/Views/AutofillTriage/Details.cshtml
|
||
| <div class="d-flex align-items-center gap-2"> | ||
| <strong>@fieldLabel</strong> | ||
| <span class="text-muted">|</span> | ||
| <span>Identified as: | ||
| @if (field.Eligible) | ||
| { | ||
| <span class="badge bg-success">@field.QualifiedAs</span> | ||
| } | ||
| else | ||
| { | ||
| <span class="badge bg-secondary">@field.QualifiedAs</span> | ||
| } | ||
| </span> | ||
| </div> | ||
| <span data-for="@collapseId" class="text-muted small">Expand</span> | ||
| </div> | ||
| <div class="collapse" id="@collapseId"> | ||
| <div class="card-body"> | ||
| <dl class="row mb-0"> | ||
| @if (field.HtmlId != null) | ||
| { | ||
| <dt class="col-sm-3">ID</dt> | ||
| <dd class="col-sm-9"><code>@field.HtmlId</code></dd> | ||
| } | ||
| @if (field.HtmlName != null) | ||
| { | ||
| <dt class="col-sm-3">Name</dt> | ||
| <dd class="col-sm-9"><code>@field.HtmlName</code></dd> | ||
| } | ||
| @if (field.HtmlType != null) | ||
| { | ||
| <dt class="col-sm-3">Type</dt> | ||
| <dd class="col-sm-9"><code>@field.HtmlType</code></dd> | ||
| } | ||
| @if (field.Placeholder != null) | ||
| { | ||
| <dt class="col-sm-3">Placeholder</dt> | ||
| <dd class="col-sm-9">@field.Placeholder</dd> | ||
| } | ||
| @if (field.AriaLabel != null) | ||
| { | ||
| <dt class="col-sm-3">ARIA Label</dt> | ||
| <dd class="col-sm-9">@field.AriaLabel</dd> | ||
| } | ||
| @if (field.Autocomplete != null) | ||
| { | ||
| <dt class="col-sm-3">Autocomplete</dt> | ||
| <dd class="col-sm-9"><code>@field.Autocomplete</code></dd> | ||
| } | ||
| @if (field.FormIndex != null) | ||
| { | ||
| <dt class="col-sm-3">Form Index</dt> | ||
| <dd class="col-sm-9">@field.FormIndex</dd> | ||
| } | ||
| </dl> | ||
|
|
||
| @if (field.Conditions.Any()) | ||
| { | ||
| <hr /> | ||
| <h6>Conditions</h6> | ||
| <ul class="list-unstyled mb-0"> | ||
| @foreach (var condition in field.Conditions) | ||
| { | ||
| <li> | ||
| @if (condition.Passed) | ||
| { | ||
| <i class="fa fa-check text-success" aria-hidden="true"></i> | ||
| } | ||
| else | ||
| { | ||
| <i class="fa fa-times text-secondary" aria-hidden="true"></i> | ||
| } | ||
| @condition.Description | ||
| </li> | ||
| } | ||
| </ul> | ||
| } | ||
| </div> | ||
| </div> | ||
| </div> | ||
| } | ||
|
|
||
| @section Scripts { | ||
| <script> | ||
| document.querySelectorAll('.collapse').forEach(function (el) { | ||
| el.addEventListener('show.bs.collapse', function () { | ||
| document.querySelector('[data-for="' + el.id + '"]').textContent = 'Collapse'; | ||
| }); | ||
| el.addEventListener('hide.bs.collapse', function () { | ||
| document.querySelector('[data-for="' + el.id + '"]').textContent = 'Expand'; | ||
| }); | ||
| }); | ||
| </script> | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| @using System.Net | ||
| @model AutofillTriageModel | ||
| @{ | ||
| ViewData["Title"] = "Autofill Triage"; | ||
| } | ||
|
|
||
| <h1>Autofill Triage Reports</h1> | ||
|
|
||
| <div class="table-responsive"> | ||
| <table class="table table-striped table-hover"> | ||
| <thead> | ||
| <tr> | ||
| <th style="white-space: nowrap;">Created At</th> | ||
| <th>Page URL</th> | ||
| <th>User Message</th> | ||
| <th></th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| @if (!Model.Items.Any()) | ||
| { | ||
| <tr> | ||
| <td colspan="4">No reports to show.</td> | ||
| </tr> | ||
| } | ||
| else | ||
| { | ||
| @foreach (var item in Model.Items) | ||
| { | ||
| var decodedUrl = WebUtility.HtmlDecode(item.PageUrl); | ||
| var truncatedUrl = decodedUrl.Length > 80 ? decodedUrl.Substring(0, 80) + "..." : decodedUrl; | ||
| var rawMessage = item.UserMessage != null ? WebUtility.HtmlDecode(item.UserMessage) : null; | ||
| var truncatedMessage = rawMessage != null && rawMessage.Length > 60 ? rawMessage.Substring(0, 60) + "..." : rawMessage; | ||
| <tr> | ||
| <td style="white-space: nowrap;"> | ||
| <a asp-action="Details" asp-route-id="@item.Id" title="@item.CreationDate.ToString()"> | ||
| @item.CreationDate.ToString("g") | ||
| </a> | ||
| </td> | ||
| <td title="@decodedUrl">@truncatedUrl</td> | ||
| <td title="@rawMessage">@(truncatedMessage ?? "—")</td> | ||
| <td class="text-end" style="width: 1%; white-space: nowrap;"> | ||
| <a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-secondary me-1">View Details</a> | ||
| <form method="post" asp-action="Archive" asp-route-id="@item.Id" style="display:inline"> | ||
| <button type="submit" | ||
| class="btn btn-sm btn-outline-danger" | ||
| onclick="return confirm('Archive this report?')"> | ||
| Archive | ||
| </button> | ||
| @Html.AntiForgeryToken() | ||
| </form> | ||
| </td> | ||
| </tr> | ||
| } | ||
| } | ||
| </tbody> | ||
| </table> | ||
| </div> | ||
|
|
||
| <nav> | ||
| <ul class="pagination"> | ||
| @if (Model.PreviousPage.HasValue) | ||
| { | ||
| <li class="page-item"> | ||
| <a class="page-link" asp-action="Index" asp-route-page="@Model.PreviousPage.Value" | ||
| asp-route-count="@Model.Count"> | ||
| Previous | ||
| </a> | ||
| </li> | ||
| } | ||
| else | ||
| { | ||
| <li class="page-item disabled"> | ||
| <a class="page-link" href="#" tabindex="-1">Previous</a> | ||
| </li> | ||
| } | ||
| @if (Model.NextPage.HasValue) | ||
| { | ||
| <li class="page-item"> | ||
| <a class="page-link" asp-action="Index" asp-route-page="@Model.NextPage.Value" | ||
| asp-route-count="@Model.Count"> | ||
| Next | ||
| </a> | ||
| </li> | ||
| } | ||
| else | ||
| { | ||
| <li class="page-item disabled"> | ||
| <a class="page-link" href="#" tabindex="-1">Next</a> | ||
| </li> | ||
| } | ||
| </ul> | ||
| </nav> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| using Bit.Api.Autofill.Models; | ||
| using Bit.Core; | ||
| using Bit.Core.Autofill.Commands; | ||
| using Bit.Core.Utilities; | ||
| using Microsoft.AspNetCore.Authorization; | ||
| using Microsoft.AspNetCore.Mvc; | ||
|
|
||
| namespace Bit.Api.Autofill.Controllers; | ||
|
|
||
| [Route("autofill/triage-report")] | ||
| [Authorize("Application")] | ||
| [RequireFeature(FeatureFlagKeys.EnableAutofillIssueReporting)] | ||
| public class AutofillTriageReportController(ICreateAutofillTriageReportCommand createAutofillTriageReportCommand) | ||
| : Controller | ||
| { | ||
| [HttpPost("")] | ||
| public async Task<IActionResult> Post([FromBody] AutofillTriageReportRequestModel model) | ||
|
kdenney marked this conversation as resolved.
|
||
| { | ||
| await createAutofillTriageReportCommand.Run(model.ToEntity()); | ||
|
kdenney marked this conversation as resolved.
|
||
| return NoContent(); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.