From 2ef9d6ac470f1110a2f674f9e92dba5c1d3ad7d5 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Wed, 27 Nov 2024 09:40:55 +1000 Subject: [PATCH] FEATURE: Allow admins to force refresh "What's new?" (#29911) Sometimes changes to "What's new?" feed items are made or the feed items are removed altogether, and the polling interval to check for new features is 1 day. This is quite long, so this commit introduces a "Check for updates" button for admins to click on the "What's new?" page which will bust the cache for the feed and check again at the new features endpoint. This is limited to 5 times per minute to avoid rapid sending of requests. --- .../admin-config-area-empty-list.gjs | 25 +++--- .../components/dashboard-new-features.gjs | 81 +++++++++++-------- .../addon/controllers/admin-whats-new.js | 14 ++++ .../admin/addon/templates/whats-new.hbs | 8 +- app/controllers/admin/dashboard_controller.rb | 14 +++- config/locales/client.en.yml | 1 + lib/discourse_updates.rb | 7 +- spec/lib/discourse_updates_spec.rb | 8 ++ .../admin/dashboard_controller_spec.rb | 32 ++++++++ 9 files changed, 142 insertions(+), 48 deletions(-) create mode 100644 app/assets/javascripts/admin/addon/controllers/admin-whats-new.js diff --git a/app/assets/javascripts/admin/addon/components/admin-config-area-empty-list.gjs b/app/assets/javascripts/admin/addon/components/admin-config-area-empty-list.gjs index 08a7009ef2c..cfcfdeb8b4f 100644 --- a/app/assets/javascripts/admin/addon/components/admin-config-area-empty-list.gjs +++ b/app/assets/javascripts/admin/addon/components/admin-config-area-empty-list.gjs @@ -1,19 +1,22 @@ +import { htmlSafe } from "@ember/template"; import DButton from "discourse/components/d-button"; import concatClass from "discourse/helpers/concat-class"; -import { i18n } from "discourse-i18n"; const AdminConfigAreaEmptyList = ; diff --git a/app/assets/javascripts/admin/addon/components/dashboard-new-features.gjs b/app/assets/javascripts/admin/addon/components/dashboard-new-features.gjs index d94c4176b7d..66b851fcb38 100644 --- a/app/assets/javascripts/admin/addon/components/dashboard-new-features.gjs +++ b/app/assets/javascripts/admin/addon/components/dashboard-new-features.gjs @@ -3,10 +3,13 @@ import { tracked } from "@glimmer/tracking"; import didInsert from "@ember/render-modifiers/modifiers/did-insert"; import { service } from "@ember/service"; import { htmlSafe } from "@ember/template"; +import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner"; import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; import { bind } from "discourse-common/utils/decorators"; import { i18n } from "discourse-i18n"; import AdminConfigAreaCard from "admin/components/admin-config-area-card"; +import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list"; import DashboardNewFeatureItem from "admin/components/dashboard-new-feature-item"; export default class DashboardNewFeatures extends Component { @@ -14,34 +17,44 @@ export default class DashboardNewFeatures extends Component { @tracked newFeatures = null; @tracked groupedNewFeatures = null; - @tracked isLoaded = false; + @tracked isLoading = true; + + constructor() { + super(...arguments); + this.args.onCheckForFeatures(this.loadNewFeatures); + } @bind - loadNewFeatures() { - ajax("/admin/whats-new.json") - .then((json) => { - const items = json.new_features.reduce((acc, feature) => { - const key = moment(feature.released_at || feature.created_at).format( - "YYYY-MM" - ); - acc[key] = acc[key] || []; - acc[key].push(feature); - return acc; - }, {}); + async loadNewFeatures(opts = {}) { + opts.forceRefresh ||= false; + this.isLoading = true; - this.groupedNewFeatures = Object.keys(items).map((date) => { - return { - date: moment - .tz(date, this.currentUser.user_option.timezone) - .format("MMMM YYYY"), - features: items[date], - }; - }); - this.isLoaded = true; - }) - .finally(() => { - this.isLoaded = true; + try { + const json = await ajax( + "/admin/whats-new.json?force_refresh=" + opts.forceRefresh + ); + const items = json.new_features.reduce((acc, feature) => { + const key = moment(feature.released_at || feature.created_at).format( + "YYYY-MM" + ); + acc[key] = acc[key] || []; + acc[key].push(feature); + return acc; + }, {}); + + this.groupedNewFeatures = Object.keys(items).map((date) => { + return { + date: moment + .tz(date, this.currentUser.user_option.timezone) + .format("MMMM YYYY"), + features: items[date], + }; }); + } catch (err) { + popupAjaxError(err); + } finally { + this.isLoading = false; + } } } diff --git a/app/assets/javascripts/admin/addon/controllers/admin-whats-new.js b/app/assets/javascripts/admin/addon/controllers/admin-whats-new.js new file mode 100644 index 00000000000..2f4a8c3daa7 --- /dev/null +++ b/app/assets/javascripts/admin/addon/controllers/admin-whats-new.js @@ -0,0 +1,14 @@ +import Controller from "@ember/controller"; +import { action } from "@ember/object"; + +export default class AdminWhatsNewController extends Controller { + @action + checkForUpdates() { + this.checkFeaturesCallback?.({ forceRefresh: true }); + } + + @action + bindCheckFeatures(checkFeaturesCallback) { + this.checkFeaturesCallback = checkFeaturesCallback; + } +} diff --git a/app/assets/javascripts/admin/addon/templates/whats-new.hbs b/app/assets/javascripts/admin/addon/templates/whats-new.hbs index 751967c07d7..854339cfc96 100644 --- a/app/assets/javascripts/admin/addon/templates/whats-new.hbs +++ b/app/assets/javascripts/admin/addon/templates/whats-new.hbs @@ -10,10 +10,16 @@ @label={{i18n "admin.dashboard.new_features.title"}} /> + <:actions as |actions|> + +
- +
\ No newline at end of file diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 1ce362d2498..2253b89abb7 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -31,7 +31,19 @@ class Admin::DashboardController < Admin::StaffController end def new_features - new_features = DiscourseUpdates.new_features + force_refresh = params[:force_refresh] == "true" + + if force_refresh + RateLimiter.new( + current_user, + "force-refresh-new-features", + 5, + 1.minute, + apply_limit_to_staff: true, + ).performed! + end + + new_features = DiscourseUpdates.new_features(force_refresh:) if current_user.admin? && most_recent = new_features&.first DiscourseUpdates.bump_last_viewed_feature_date(current_user.id, most_recent["created_at"]) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index eaef3cbaf38..5accd159f38 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -5115,6 +5115,7 @@ en: new_features: title: "What's new" + check_for_updates: "Check for updates" dashboard: title: "Dashboard" last_updated: "Dashboard updated:" diff --git a/lib/discourse_updates.rb b/lib/discourse_updates.rb index 3ddae519197..b280728740a 100644 --- a/lib/discourse_updates.rb +++ b/lib/discourse_updates.rb @@ -132,7 +132,8 @@ module DiscourseUpdates end def new_features_payload - response = Excon.new(new_features_endpoint).request(expects: [200], method: :Get) + response = + Excon.new(new_features_endpoint).request(expects: [200], method: :Get, read_timeout: 5) response.body end @@ -141,7 +142,9 @@ module DiscourseUpdates Discourse.redis.set(new_features_key, payload) end - def new_features + def new_features(force_refresh: false) + update_new_features if force_refresh + entries = begin JSON.parse(Discourse.redis.get(new_features_key)) diff --git a/spec/lib/discourse_updates_spec.rb b/spec/lib/discourse_updates_spec.rb index c53b362f461..3b5dd23adba 100644 --- a/spec/lib/discourse_updates_spec.rb +++ b/spec/lib/discourse_updates_spec.rb @@ -308,6 +308,14 @@ RSpec.describe DiscourseUpdates do expect(result[0]["title"]).to eq("Bells") expect(result[1]["title"]).to eq("Whistles") end + + it "correctly refetches features if force_refresh is used" do + DiscourseUpdates.expects(:update_new_features).once + result = DiscourseUpdates.new_features + expect(result.length).to eq(3) + result = DiscourseUpdates.new_features(force_refresh: true) + expect(result.length).to eq(3) + end end describe "#get_last_viewed_feature_date" do diff --git a/spec/requests/admin/dashboard_controller_spec.rb b/spec/requests/admin/dashboard_controller_spec.rb index 1847e41a7b4..42c7311ccc3 100644 --- a/spec/requests/admin/dashboard_controller_spec.rb +++ b/spec/requests/admin/dashboard_controller_spec.rb @@ -191,6 +191,38 @@ RSpec.describe Admin::DashboardController do expect(json["has_unseen_features"]).to eq(true) end + it "allows for forcing a refresh of new features, busting the cache" do + populate_new_features + + get "/admin/whats-new.json" + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["new_features"].length).to eq(2) + + get "/admin/whats-new.json" + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["new_features"].length).to eq(2) + + DiscourseUpdates.stubs(:new_features_payload).returns( + [ + { + "id" => "3", + "emoji" => "🚀", + "title" => "Space platform launched!", + "description" => "Now to make it to the next planet unscathed...", + "created_at" => 1.minute.ago, + }, + ].to_json, + ) + + get "/admin/whats-new.json?force_refresh=true" + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["new_features"].length).to eq(1) + expect(json["new_features"][0]["id"]).to eq("3") + end + it "passes unseen feature state" do populate_new_features DiscourseUpdates.mark_new_features_as_seen(admin.id)