mirror of
https://github.com/discourse/discourse.git
synced 2024-11-28 08:23:43 +08:00
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.
This commit is contained in:
parent
b9f183e2c3
commit
2ef9d6ac47
|
@ -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 = <template>
|
||||
<div class="admin-config-area-empty-list">
|
||||
{{i18n @emptyLabel}}
|
||||
<DButton
|
||||
@label={{@ctaLabel}}
|
||||
class={{concatClass
|
||||
"btn-default btn-small admin-config-area-empty-list__cta-button"
|
||||
@ctaClass
|
||||
}}
|
||||
@action={{@ctaAction}}
|
||||
@route={{@ctaRoute}}
|
||||
/>
|
||||
{{htmlSafe @emptyLabel}}
|
||||
|
||||
{{#if @ctaLabel}}
|
||||
<DButton
|
||||
@label={{@ctaLabel}}
|
||||
class={{concatClass
|
||||
"btn-default btn-small admin-config-area-empty-list__cta-button"
|
||||
@ctaClass
|
||||
}}
|
||||
@action={{@ctaAction}}
|
||||
@route={{@ctaRoute}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
|
@ -49,7 +62,7 @@ export default class DashboardNewFeatures extends Component {
|
|||
class="admin-config-area__primary-content"
|
||||
{{didInsert this.loadNewFeatures}}
|
||||
>
|
||||
{{#if this.groupedNewFeatures}}
|
||||
<ConditionalLoadingSpinner @condition={{this.isLoading}}>
|
||||
{{#each this.groupedNewFeatures as |groupedFeatures|}}
|
||||
<AdminConfigAreaCard @translatedHeading={{groupedFeatures.date}}>
|
||||
<:content>
|
||||
|
@ -58,15 +71,17 @@ export default class DashboardNewFeatures extends Component {
|
|||
{{/each}}
|
||||
</:content>
|
||||
</AdminConfigAreaCard>
|
||||
{{else}}
|
||||
<AdminConfigAreaEmptyList
|
||||
@emptyLabel={{htmlSafe
|
||||
(i18n
|
||||
"admin.dashboard.new_features.previous_announcements"
|
||||
url="https://meta.discourse.org/tags/c/announcements/67/release-notes"
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{else if this.isLoaded}}
|
||||
{{htmlSafe
|
||||
(i18n
|
||||
"admin.dashboard.new_features.previous_announcements"
|
||||
url="https://meta.discourse.org/tags/c/announcements/67/release-notes"
|
||||
)
|
||||
}}
|
||||
{{/if}}
|
||||
</ConditionalLoadingSpinner>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -10,10 +10,16 @@
|
|||
@label={{i18n "admin.dashboard.new_features.title"}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
<:actions as |actions|>
|
||||
<actions.Primary
|
||||
@label="admin.new_features.check_for_updates"
|
||||
@action={{this.checkForUpdates}}
|
||||
/>
|
||||
</:actions>
|
||||
</AdminPageHeader>
|
||||
|
||||
<div class="admin-container admin-config-page__main-area">
|
||||
<div class="admin-config-area">
|
||||
<DashboardNewFeatures />
|
||||
<DashboardNewFeatures @onCheckForFeatures={{this.bindCheckFeatures}} />
|
||||
</div>
|
||||
</div>
|
|
@ -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"])
|
||||
|
|
|
@ -5115,6 +5115,7 @@ en:
|
|||
|
||||
new_features:
|
||||
title: "What's new"
|
||||
check_for_updates: "Check for updates"
|
||||
dashboard:
|
||||
title: "Dashboard"
|
||||
last_updated: "Dashboard updated:"
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user