mirror of
https://github.com/discourse/discourse.git
synced 2025-02-26 10:17:23 +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 DButton from "discourse/components/d-button";
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
import { i18n } from "discourse-i18n";
|
|
||||||
|
|
||||||
const AdminConfigAreaEmptyList = <template>
|
const AdminConfigAreaEmptyList = <template>
|
||||||
<div class="admin-config-area-empty-list">
|
<div class="admin-config-area-empty-list">
|
||||||
{{i18n @emptyLabel}}
|
{{htmlSafe @emptyLabel}}
|
||||||
<DButton
|
|
||||||
@label={{@ctaLabel}}
|
{{#if @ctaLabel}}
|
||||||
class={{concatClass
|
<DButton
|
||||||
"btn-default btn-small admin-config-area-empty-list__cta-button"
|
@label={{@ctaLabel}}
|
||||||
@ctaClass
|
class={{concatClass
|
||||||
}}
|
"btn-default btn-small admin-config-area-empty-list__cta-button"
|
||||||
@action={{@ctaAction}}
|
@ctaClass
|
||||||
@route={{@ctaRoute}}
|
}}
|
||||||
/>
|
@action={{@ctaAction}}
|
||||||
|
@route={{@ctaRoute}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</template>;
|
</template>;
|
||||||
|
|
||||||
|
@ -3,10 +3,13 @@ import { tracked } from "@glimmer/tracking";
|
|||||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import { i18n } from "discourse-i18n";
|
import { i18n } from "discourse-i18n";
|
||||||
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
|
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";
|
import DashboardNewFeatureItem from "admin/components/dashboard-new-feature-item";
|
||||||
|
|
||||||
export default class DashboardNewFeatures extends Component {
|
export default class DashboardNewFeatures extends Component {
|
||||||
@ -14,34 +17,44 @@ export default class DashboardNewFeatures extends Component {
|
|||||||
|
|
||||||
@tracked newFeatures = null;
|
@tracked newFeatures = null;
|
||||||
@tracked groupedNewFeatures = null;
|
@tracked groupedNewFeatures = null;
|
||||||
@tracked isLoaded = false;
|
@tracked isLoading = true;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.args.onCheckForFeatures(this.loadNewFeatures);
|
||||||
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
loadNewFeatures() {
|
async loadNewFeatures(opts = {}) {
|
||||||
ajax("/admin/whats-new.json")
|
opts.forceRefresh ||= false;
|
||||||
.then((json) => {
|
this.isLoading = true;
|
||||||
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) => {
|
try {
|
||||||
return {
|
const json = await ajax(
|
||||||
date: moment
|
"/admin/whats-new.json?force_refresh=" + opts.forceRefresh
|
||||||
.tz(date, this.currentUser.user_option.timezone)
|
);
|
||||||
.format("MMMM YYYY"),
|
const items = json.new_features.reduce((acc, feature) => {
|
||||||
features: items[date],
|
const key = moment(feature.released_at || feature.created_at).format(
|
||||||
};
|
"YYYY-MM"
|
||||||
});
|
);
|
||||||
this.isLoaded = true;
|
acc[key] = acc[key] || [];
|
||||||
})
|
acc[key].push(feature);
|
||||||
.finally(() => {
|
return acc;
|
||||||
this.isLoaded = true;
|
}, {});
|
||||||
|
|
||||||
|
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>
|
<template>
|
||||||
@ -49,7 +62,7 @@ export default class DashboardNewFeatures extends Component {
|
|||||||
class="admin-config-area__primary-content"
|
class="admin-config-area__primary-content"
|
||||||
{{didInsert this.loadNewFeatures}}
|
{{didInsert this.loadNewFeatures}}
|
||||||
>
|
>
|
||||||
{{#if this.groupedNewFeatures}}
|
<ConditionalLoadingSpinner @condition={{this.isLoading}}>
|
||||||
{{#each this.groupedNewFeatures as |groupedFeatures|}}
|
{{#each this.groupedNewFeatures as |groupedFeatures|}}
|
||||||
<AdminConfigAreaCard @translatedHeading={{groupedFeatures.date}}>
|
<AdminConfigAreaCard @translatedHeading={{groupedFeatures.date}}>
|
||||||
<:content>
|
<:content>
|
||||||
@ -58,15 +71,17 @@ export default class DashboardNewFeatures extends Component {
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
</:content>
|
</:content>
|
||||||
</AdminConfigAreaCard>
|
</AdminConfigAreaCard>
|
||||||
|
{{else}}
|
||||||
|
<AdminConfigAreaEmptyList
|
||||||
|
@emptyLabel={{htmlSafe
|
||||||
|
(i18n
|
||||||
|
"admin.dashboard.new_features.previous_announcements"
|
||||||
|
url="https://meta.discourse.org/tags/c/announcements/67/release-notes"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else if this.isLoaded}}
|
</ConditionalLoadingSpinner>
|
||||||
{{htmlSafe
|
|
||||||
(i18n
|
|
||||||
"admin.dashboard.new_features.previous_announcements"
|
|
||||||
url="https://meta.discourse.org/tags/c/announcements/67/release-notes"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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"}}
|
@label={{i18n "admin.dashboard.new_features.title"}}
|
||||||
/>
|
/>
|
||||||
</:breadcrumbs>
|
</:breadcrumbs>
|
||||||
|
<:actions as |actions|>
|
||||||
|
<actions.Primary
|
||||||
|
@label="admin.new_features.check_for_updates"
|
||||||
|
@action={{this.checkForUpdates}}
|
||||||
|
/>
|
||||||
|
</:actions>
|
||||||
</AdminPageHeader>
|
</AdminPageHeader>
|
||||||
|
|
||||||
<div class="admin-container admin-config-page__main-area">
|
<div class="admin-container admin-config-page__main-area">
|
||||||
<div class="admin-config-area">
|
<div class="admin-config-area">
|
||||||
<DashboardNewFeatures />
|
<DashboardNewFeatures @onCheckForFeatures={{this.bindCheckFeatures}} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -31,7 +31,19 @@ class Admin::DashboardController < Admin::StaffController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def new_features
|
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
|
if current_user.admin? && most_recent = new_features&.first
|
||||||
DiscourseUpdates.bump_last_viewed_feature_date(current_user.id, most_recent["created_at"])
|
DiscourseUpdates.bump_last_viewed_feature_date(current_user.id, most_recent["created_at"])
|
||||||
|
@ -5115,6 +5115,7 @@ en:
|
|||||||
|
|
||||||
new_features:
|
new_features:
|
||||||
title: "What's new"
|
title: "What's new"
|
||||||
|
check_for_updates: "Check for updates"
|
||||||
dashboard:
|
dashboard:
|
||||||
title: "Dashboard"
|
title: "Dashboard"
|
||||||
last_updated: "Dashboard updated:"
|
last_updated: "Dashboard updated:"
|
||||||
|
@ -132,7 +132,8 @@ module DiscourseUpdates
|
|||||||
end
|
end
|
||||||
|
|
||||||
def new_features_payload
|
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
|
response.body
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -141,7 +142,9 @@ module DiscourseUpdates
|
|||||||
Discourse.redis.set(new_features_key, payload)
|
Discourse.redis.set(new_features_key, payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_features
|
def new_features(force_refresh: false)
|
||||||
|
update_new_features if force_refresh
|
||||||
|
|
||||||
entries =
|
entries =
|
||||||
begin
|
begin
|
||||||
JSON.parse(Discourse.redis.get(new_features_key))
|
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[0]["title"]).to eq("Bells")
|
||||||
expect(result[1]["title"]).to eq("Whistles")
|
expect(result[1]["title"]).to eq("Whistles")
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "#get_last_viewed_feature_date" do
|
describe "#get_last_viewed_feature_date" do
|
||||||
|
@ -191,6 +191,38 @@ RSpec.describe Admin::DashboardController do
|
|||||||
expect(json["has_unseen_features"]).to eq(true)
|
expect(json["has_unseen_features"]).to eq(true)
|
||||||
end
|
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
|
it "passes unseen feature state" do
|
||||||
populate_new_features
|
populate_new_features
|
||||||
DiscourseUpdates.mark_new_features_as_seen(admin.id)
|
DiscourseUpdates.mark_new_features_as_seen(admin.id)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user