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)