FEATURE: Add ability to dismiss admin notices (#28916)
his is a new feature that lets admins dismiss notices from the dashboard. This helps with self-service in cases where a notice is "stuck", while we work on provisions to prevent "sticking" in the first place.
This commit is contained in:
parent
d7a46e1702
commit
be33363f13
|
@ -0,0 +1,25 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
|
||||
export default class AdminNotice extends Component {
|
||||
@action
|
||||
dismiss() {
|
||||
this.args.dismissCallback(this.args.problem);
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="notice">
|
||||
<div class="message">
|
||||
{{if @icon (icon @icon)}}
|
||||
{{htmlSafe @problem.message}}
|
||||
</div>
|
||||
<DButton
|
||||
@action={{this.dismiss}}
|
||||
@label="admin.dashboard.dismiss_notice"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -1,15 +1,33 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { concat } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { eq } from "truth-helpers";
|
||||
import ConditionalLoadingSection from "discourse/components/conditional-loading-section";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import AdminNotice from "admin/components/admin-notice";
|
||||
|
||||
// eslint-disable-next-line ember/no-empty-glimmer-component-classes
|
||||
export default class DashboardProblems extends Component {
|
||||
@action
|
||||
async dismissProblem(problem) {
|
||||
try {
|
||||
await ajax(`/admin/admin_notices/${problem.id}`, { type: "DELETE" });
|
||||
this.args.problems.removeObject(problem);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
get problems() {
|
||||
return this.args.problems.sortBy("priority");
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if @foundProblems}}
|
||||
{{#if @problems.length}}
|
||||
<div class="section dashboard-problems">
|
||||
<div class="section-title">
|
||||
<h2>
|
||||
|
@ -20,35 +38,24 @@ export default class DashboardProblems extends Component {
|
|||
|
||||
<div class="section-body">
|
||||
<ConditionalLoadingSection @isLoading={{@loadingProblems}}>
|
||||
{{#if @highPriorityProblems.length}}
|
||||
<div class="problem-messages priority-high">
|
||||
<ul>
|
||||
{{#each @highPriorityProblems as |problem|}}
|
||||
<li
|
||||
class={{concatClass
|
||||
"dashboard-problem "
|
||||
"priority-"
|
||||
problem.priority
|
||||
}}
|
||||
>
|
||||
{{icon "triangle-exclamation"}}
|
||||
{{htmlSafe problem.message}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="problem-messages priority-low">
|
||||
<div class="problem-messages">
|
||||
<ul>
|
||||
{{#each @lowPriorityProblems as |problem|}}
|
||||
{{#each this.problems as |problem|}}
|
||||
<li
|
||||
class={{concatClass
|
||||
"dashboard-problem "
|
||||
"priority-"
|
||||
problem.priority
|
||||
"dashboard-problem"
|
||||
(concat "priority-" problem.priority)
|
||||
}}
|
||||
>{{htmlSafe problem.message}}</li>
|
||||
>
|
||||
<AdminNotice
|
||||
@icon={{if
|
||||
(eq problem.priority "high")
|
||||
"triangle-exclamation"
|
||||
}}
|
||||
@problem={{problem}}
|
||||
@dismissCallback={{this.dismissProblem}}
|
||||
/>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -18,16 +18,6 @@ export default class AdminDashboardController extends Controller {
|
|||
|
||||
@setting("version_checks") showVersionChecks;
|
||||
|
||||
@discourseComputed(
|
||||
"lowPriorityProblems.length",
|
||||
"highPriorityProblems.length"
|
||||
)
|
||||
foundProblems(lowPriorityProblemsLength, highPriorityProblemsLength) {
|
||||
const problemsLength =
|
||||
lowPriorityProblemsLength + highPriorityProblemsLength;
|
||||
return this.currentUser.admin && problemsLength > 0;
|
||||
}
|
||||
|
||||
@computed("siteSettings.dashboard_visible_tabs")
|
||||
get visibleTabs() {
|
||||
return (this.siteSettings.dashboard_visible_tabs || "")
|
||||
|
@ -106,16 +96,7 @@ export default class AdminDashboardController extends Controller {
|
|||
});
|
||||
|
||||
AdminDashboard.fetchProblems()
|
||||
.then((model) => {
|
||||
this.set(
|
||||
"highPriorityProblems",
|
||||
model.problems.filterBy("priority", "high")
|
||||
);
|
||||
this.set(
|
||||
"lowPriorityProblems",
|
||||
model.problems.filterBy("priority", "low")
|
||||
);
|
||||
})
|
||||
.then((model) => this.set("problems", model.problems))
|
||||
.finally(() => this.set("loadingProblems", false));
|
||||
}
|
||||
|
||||
|
|
|
@ -18,9 +18,7 @@
|
|||
|
||||
<DashboardProblems
|
||||
@loadingProblems={{this.loadingProblems}}
|
||||
@foundProblems={{this.foundProblems}}
|
||||
@lowPriorityProblems={{this.lowPriorityProblems}}
|
||||
@highPriorityProblems={{this.highPriorityProblems}}
|
||||
@problems={{this.problems}}
|
||||
@problemsTimestamp={{this.problemsTimestamp}}
|
||||
@refreshProblems={{action "refreshProblems"}}
|
||||
/>
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
acceptance,
|
||||
count,
|
||||
exists,
|
||||
query,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
|
||||
|
@ -60,13 +59,6 @@ acceptance("Dashboard", function (needs) {
|
|||
exists(".admin-report.new-contributors"),
|
||||
"new-contributors report"
|
||||
);
|
||||
assert.strictEqual(
|
||||
query(
|
||||
".section.dashboard-problems .problem-messages ul li:first-child"
|
||||
).innerHTML.trim(),
|
||||
"Houston...",
|
||||
"displays problems"
|
||||
);
|
||||
});
|
||||
|
||||
test("moderation tab", async function (assert) {
|
||||
|
|
|
@ -246,16 +246,21 @@
|
|||
.problem-messages {
|
||||
margin-bottom: 1em;
|
||||
|
||||
&.priority-high {
|
||||
background-color: var(--danger-low);
|
||||
border: 1px solid var(--danger-medium);
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0 0 0 1.25em;
|
||||
|
||||
li.dashboard-problem {
|
||||
padding: 0.5em 0.5em;
|
||||
|
||||
.notice {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-right: var(--space-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::AdminNoticesController < Admin::StaffController
|
||||
def destroy
|
||||
AdminNotices::Dismiss.call do
|
||||
on_success { render(json: success_json) }
|
||||
on_failure { render(json: failed_json, status: 422) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -32,11 +32,14 @@ class ProblemCheckTracker < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def no_problem!(next_run_at: nil)
|
||||
reset
|
||||
silence_the_alarm
|
||||
end
|
||||
|
||||
def reset(next_run_at: nil)
|
||||
now = Time.current
|
||||
|
||||
update!(blips: 0, last_run_at: now, last_success_at: now, next_run_at:)
|
||||
|
||||
silence_the_alarm
|
||||
end
|
||||
|
||||
def check
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AdminNoticeSerializer < ApplicationSerializer
|
||||
attributes :priority, :message, :identifier
|
||||
attributes :id, :priority, :message, :identifier
|
||||
end
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AdminNotices::Dismiss
|
||||
include Service::Base
|
||||
|
||||
model :admin_notice
|
||||
|
||||
policy :invalid_access
|
||||
|
||||
transaction do
|
||||
step :destroy
|
||||
step :reset_problem_check
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_admin_notice(id:)
|
||||
AdminNotice.find_by(id: id)
|
||||
end
|
||||
|
||||
def invalid_access(guardian:)
|
||||
guardian.is_admin?
|
||||
end
|
||||
|
||||
def destroy(admin_notice:)
|
||||
admin_notice.destroy!
|
||||
end
|
||||
|
||||
def reset_problem_check(admin_notice:)
|
||||
ProblemCheckTracker.find_by(identifier: admin_notice.identifier)&.reset
|
||||
end
|
||||
end
|
|
@ -5077,6 +5077,7 @@ en:
|
|||
installed_version: "Installed"
|
||||
latest_version: "Latest"
|
||||
problems_found: "Some advice based on your current site settings"
|
||||
dismiss_notice: "Dismiss"
|
||||
new_features:
|
||||
title: "What's new"
|
||||
subtitle: "We are releasing new features and improvements all the time. This page covers the highlights, but you can click 'Learn more' to see extensive release notes."
|
||||
|
|
|
@ -401,6 +401,8 @@ Discourse::Application.routes.draw do
|
|||
collection { put "/" => "about#update" }
|
||||
end
|
||||
end
|
||||
|
||||
resources :admin_notices, only: %i[destroy], constraints: AdminConstraint.new
|
||||
end # admin namespace
|
||||
|
||||
get "email/unsubscribe/:key" => "email#unsubscribe", :as => "email_unsubscribe"
|
||||
|
|
|
@ -3,4 +3,5 @@
|
|||
Fabricator(:admin_notice) do
|
||||
priority { "low" }
|
||||
identifier { "test_notice" }
|
||||
subject { "problem" }
|
||||
end
|
||||
|
|
|
@ -212,4 +212,30 @@ RSpec.describe ProblemCheckTracker do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#reset" do
|
||||
let(:problem_tracker) do
|
||||
Fabricate(:problem_check_tracker, identifier: "twitter_login", **original_attributes)
|
||||
end
|
||||
|
||||
let(:original_attributes) do
|
||||
{
|
||||
blips: 0,
|
||||
last_problem_at: 1.week.ago,
|
||||
last_success_at: Time.current,
|
||||
last_run_at: 24.hours.ago,
|
||||
next_run_at: nil,
|
||||
}
|
||||
end
|
||||
|
||||
let(:updated_attributes) { { blips: 0 } }
|
||||
|
||||
it do
|
||||
freeze_time
|
||||
|
||||
expect { problem_tracker.reset(next_run_at: 24.hours.from_now) }.to change {
|
||||
problem_tracker.attributes
|
||||
}.to(hash_including(updated_attributes))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe(AdminNotices::Dismiss) do
|
||||
subject(:result) { described_class.call(id: admin_notice.id, guardian: current_user.guardian) }
|
||||
|
||||
let!(:admin_notice) { Fabricate(:admin_notice, identifier: "problem.test") }
|
||||
let!(:problem_check) { Fabricate(:problem_check_tracker, identifier: "problem.test", blips: 3) }
|
||||
|
||||
context "when user is not allowed to perform the action" do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
|
||||
it { is_expected.to fail_a_policy(:invalid_access) }
|
||||
end
|
||||
|
||||
context "when user is allowed to perform the action" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
|
||||
it { is_expected.to run_successfully }
|
||||
|
||||
it "sets the service result as successful" do
|
||||
expect(result).to be_a_success
|
||||
end
|
||||
|
||||
it "destroys the admin notice" do
|
||||
expect { result }.to change { AdminNotice.count }.from(1).to(0)
|
||||
end
|
||||
|
||||
it "resets any associated problem check" do
|
||||
expect { result }.to change { problem_check.reload.blips }.from(3).to(0)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
describe "Admin Notices", type: :system do
|
||||
fab!(:admin)
|
||||
|
||||
let(:admin_dashboard) { PageObjects::Pages::AdminDashboard.new }
|
||||
|
||||
before do
|
||||
Fabricate(:admin_notice)
|
||||
|
||||
I18n.backend.store_translations(:en, dashboard: { problem: { test_notice: "Houston" } })
|
||||
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
it "supports dismissing admin notices" do
|
||||
admin_dashboard.visit
|
||||
|
||||
expect(admin_dashboard).to have_admin_notice(I18n.t("dashboard.problem.test_notice"))
|
||||
|
||||
admin_dashboard.dismiss_notice(I18n.t("dashboard.problem.test_notice"))
|
||||
|
||||
expect(admin_dashboard).to have_no_admin_notice(I18n.t("dashboard.problem.test_notice"))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class AdminDashboard < PageObjects::Pages::Base
|
||||
def visit
|
||||
page.visit("/admin")
|
||||
self
|
||||
end
|
||||
|
||||
def has_admin_notice?(message)
|
||||
has_css?(".dashboard-problem", text: message)
|
||||
end
|
||||
|
||||
def has_no_admin_notice?(message)
|
||||
has_no_css?(".dashboard-problem", text: message)
|
||||
end
|
||||
|
||||
def dismiss_notice(message)
|
||||
find(".dashboard-problem", text: message).find(".btn").click
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue