FEATURE: Add new features section in admin dashboard (#11731)
This commit is contained in:
parent
71656d2c37
commit
4f01ca87e3
|
@ -0,0 +1,26 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
|
||||
export default Component.extend({
|
||||
newFeatures: null,
|
||||
releaseNotesLink: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
ajax("/admin/dashboard/new-features.json").then((json) => {
|
||||
this.setProperties({
|
||||
newFeatures: json.new_features,
|
||||
releaseNotesLink: json.release_notes_link,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
dismissNewFeatures() {
|
||||
ajax("/admin/dashboard/mark-new-features-as-seen.json", {
|
||||
type: "PUT",
|
||||
}).then(() => this.set("newFeatures", null));
|
||||
},
|
||||
});
|
|
@ -14,6 +14,7 @@ AdminDashboard.reopenClass({
|
|||
return ajax("/admin/dashboard.json").then((json) => {
|
||||
const model = AdminDashboard.create();
|
||||
model.set("version_check", json.version_check);
|
||||
|
||||
return model;
|
||||
});
|
||||
},
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<div class="admin-new-feature-item">
|
||||
<div class="new-feature-emoji">{{item.emoji}}</div>
|
||||
<div class="new-feature-content">
|
||||
<div class="header">
|
||||
{{#if item.link}}
|
||||
<a href={{item.link}} target="_blank" rel="noopener noreferrer">{{item.title}}</a>
|
||||
{{else}}
|
||||
{{item.title}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="feature-description">{{item.description}}</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,21 @@
|
|||
{{#if newFeatures}}
|
||||
<div class="section dashboard-new-features">
|
||||
<div class="section-title">
|
||||
<h2>{{replace-emoji (i18n "admin.dashboard.new_features.title") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="section-body">
|
||||
{{#each newFeatures as |feature|}}
|
||||
{{dashboard-new-feature-item item=feature}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="section-footer">
|
||||
{{#if releaseNotesLink}}
|
||||
<a rel="noopener noreferrer" target="_blank" href={{releaseNotesLink}} class="btn btn-primary new-features-release-notes">
|
||||
{{i18n "admin.dashboard.new_features.learn_more"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
{{d-button label="admin.dashboard.new_features.dismiss" class="new-features-dismiss" action=dismissNewFeatures }}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -1,3 +1,5 @@
|
|||
{{dashboard-new-features}}
|
||||
|
||||
{{plugin-outlet name="admin-dashboard-top"}}
|
||||
|
||||
{{#if showVersionChecks}}
|
||||
|
|
|
@ -127,6 +127,13 @@ acceptance("Dashboard", function (needs) {
|
|||
"its set the value of the filter from the query params"
|
||||
);
|
||||
});
|
||||
|
||||
test("new features", async function (assert) {
|
||||
await visit("/admin");
|
||||
|
||||
assert.ok(exists(".dashboard-new-features"));
|
||||
assert.ok(exists(".dashboard-new-features .new-features-release-notes"));
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Dashboard: dashboard_visible_tabs", function (needs) {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
export default {
|
||||
"/admin/dashboard/new-features.json": {
|
||||
new_features: [
|
||||
{
|
||||
id: 1,
|
||||
user_id: 127,
|
||||
emoji: "😎",
|
||||
title: "New color palettes",
|
||||
description:
|
||||
"New light and dark color palettes that adhere to Web Content Accessibility Guidelines. ",
|
||||
tier: [],
|
||||
link: "https://meta.discourse.org",
|
||||
created_at: "2021-01-18T19:59:29.666Z",
|
||||
updated_at: "2021-01-19T19:33:16.150Z",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
user_id: 127,
|
||||
emoji: "👱♀️",
|
||||
title: "Suspend users quickly",
|
||||
description:
|
||||
"Staff can now suspend or silence a user immediately, without needing to visit the review queue or admin page. ",
|
||||
tier: [],
|
||||
link: "",
|
||||
created_at: "2021-01-19T19:20:09.757Z",
|
||||
updated_at: "2021-01-19T19:20:09.757Z",
|
||||
}
|
||||
],
|
||||
release_notes_link:
|
||||
"https://meta.discourse.org/c/feature/announcements?tags=release-notes\u0026before=0",
|
||||
},
|
||||
};
|
|
@ -612,3 +612,42 @@
|
|||
font-size: $font-up-3;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-new-features {
|
||||
.section-body {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
grid-gap: 1.5em;
|
||||
}
|
||||
|
||||
.section-footer {
|
||||
margin: 1.5em;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
.btn {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-new-feature-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.new-feature-emoji {
|
||||
font-size: 3.5em;
|
||||
padding-right: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.new-feature-content {
|
||||
padding-right: 0.5em;
|
||||
align-self: center;
|
||||
.header {
|
||||
font-size: $font-up-1;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,4 +22,15 @@ class Admin::DashboardController < Admin::AdminController
|
|||
def problems
|
||||
render_json_dump(problems: AdminDashboardData.fetch_problems(check_force_https: request.ssl?))
|
||||
end
|
||||
|
||||
def new_features
|
||||
data = { new_features: DiscourseUpdates.unseen_new_features(current_user.id) }
|
||||
data.merge!(release_notes_link: AdminDashboardGeneralData.fetch_cached_stats["release_notes_link"])
|
||||
render json: data
|
||||
end
|
||||
|
||||
def mark_new_features_as_seen
|
||||
DiscourseUpdates.mark_new_features_as_seen(current_user.id)
|
||||
render json: success_json
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3601,6 +3601,10 @@ en:
|
|||
installed_version: "Installed"
|
||||
latest_version: "Latest"
|
||||
problems_found: "Some advice based on your current site settings"
|
||||
new_features:
|
||||
title: "🎁 New Features"
|
||||
dismiss: "Dismiss"
|
||||
learn_more: "Learn more"
|
||||
last_checked: "Last checked"
|
||||
refresh_problems: "Refresh"
|
||||
no_problems: "No problems were found."
|
||||
|
|
|
@ -260,6 +260,8 @@ Discourse::Application.routes.draw do
|
|||
get "dashboard/moderation" => "dashboard#moderation"
|
||||
get "dashboard/security" => "dashboard#security"
|
||||
get "dashboard/reports" => "dashboard#reports"
|
||||
get "dashboard/new-features" => "dashboard#new_features"
|
||||
put "dashboard/mark-new-features-as-seen" => "dashboard#mark_new_features_as_seen"
|
||||
|
||||
resources :dashboard, only: [:index] do
|
||||
collection do
|
||||
|
|
|
@ -2134,6 +2134,10 @@ uncategorized:
|
|||
client: true
|
||||
hidden: true
|
||||
|
||||
check_for_new_features:
|
||||
default: false
|
||||
hidden: true
|
||||
|
||||
automatically_unpin_topics:
|
||||
default: true
|
||||
client: true
|
||||
|
|
|
@ -115,6 +115,38 @@ module DiscourseUpdates
|
|||
keys.present? ? keys.map { |k| Discourse.redis.hgetall(k) } : []
|
||||
end
|
||||
|
||||
def perform_new_feature_check
|
||||
response = Excon.new(new_features_endpoint).request(expects: [200], method: :Get)
|
||||
json = JSON.parse(response.body)
|
||||
Discourse.redis.set(new_features_key, response.body)
|
||||
end
|
||||
|
||||
def unseen_new_features(user_id)
|
||||
entries = JSON.parse(Discourse.redis.get(new_features_key)) rescue nil
|
||||
return nil if entries.nil?
|
||||
|
||||
last_seen = new_features_last_seen(user_id)
|
||||
|
||||
if last_seen.present?
|
||||
entries.select! { |item| Time.zone.parse(item["created_at"]) > last_seen }
|
||||
end
|
||||
|
||||
entries.sort { |item| Time.zone.parse(item["created_at"]) }
|
||||
end
|
||||
|
||||
def new_features_last_seen(user_id)
|
||||
last_seen = Discourse.redis.get new_features_last_seen_key(user_id)
|
||||
return nil if last_seen.blank?
|
||||
Time.zone.parse(last_seen)
|
||||
end
|
||||
|
||||
def mark_new_features_as_seen(user_id)
|
||||
entries = JSON.parse(Discourse.redis.get(new_features_key)) rescue nil
|
||||
return nil if entries.nil?
|
||||
last_seen = entries.max_by { |x| x["created_at"] }
|
||||
Discourse.redis.set(new_features_last_seen_key(user_id), last_seen["created_at"])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def last_installed_version_key
|
||||
|
@ -144,5 +176,17 @@ module DiscourseUpdates
|
|||
def missing_versions_key_prefix
|
||||
'missing_version'
|
||||
end
|
||||
|
||||
def new_features_endpoint
|
||||
'https://meta.discourse.org/new-features.json'
|
||||
end
|
||||
|
||||
def new_features_key
|
||||
'new_features'
|
||||
end
|
||||
|
||||
def new_features_last_seen_key(user_id)
|
||||
"new_features_last_seen_user_#{user_id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -144,4 +144,72 @@ describe DiscourseUpdates do
|
|||
include_examples "when last_installed_version is old"
|
||||
end
|
||||
end
|
||||
|
||||
context 'new features' do
|
||||
fab!(:admin) { Fabricate(:admin) }
|
||||
fab!(:admin2) { Fabricate(:admin) }
|
||||
let!(:last_item_date) { 5.minutes.ago }
|
||||
let!(:sample_features) { [
|
||||
{ "emoji" => "🤾", "title" => "Super Fruits", "description" => "Taste explosion!", "created_at" => 40.minutes.ago },
|
||||
{ "emoji" => "🙈", "title" => "Fancy Legumes", "description" => "Magic legumes!", "created_at" => 15.minutes.ago },
|
||||
{ "emoji" => "🤾", "title" => "Quality Veggies", "description" => "Green goodness!", "created_at" => last_item_date },
|
||||
] }
|
||||
|
||||
before(:each) do
|
||||
Discourse.redis.del "new_features_last_seen_user_#{admin.id}"
|
||||
Discourse.redis.del "new_features_last_seen_user_#{admin2.id}"
|
||||
Discourse.redis.del "new_features"
|
||||
|
||||
Discourse.redis.set('new_features', MultiJson.dump(sample_features))
|
||||
end
|
||||
|
||||
it 'returns all items on the first run' do
|
||||
result = DiscourseUpdates.unseen_new_features(admin.id)
|
||||
|
||||
expect(result.length).to eq(3)
|
||||
expect(result[2]["title"]).to eq("Super Fruits")
|
||||
end
|
||||
|
||||
it 'returns only unseen items by user' do
|
||||
DiscourseUpdates.stubs(:new_features_last_seen).with(admin.id).returns(10.minutes.ago)
|
||||
DiscourseUpdates.stubs(:new_features_last_seen).with(admin2.id).returns(30.minutes.ago)
|
||||
|
||||
result = DiscourseUpdates.unseen_new_features(admin.id)
|
||||
expect(result.length).to eq(1)
|
||||
expect(result[0]["title"]).to eq("Quality Veggies")
|
||||
|
||||
result2 = DiscourseUpdates.unseen_new_features(admin2.id)
|
||||
expect(result2.length).to eq(2)
|
||||
expect(result2[0]["title"]).to eq("Quality Veggies")
|
||||
expect(result2[1]["title"]).to eq("Fancy Legumes")
|
||||
end
|
||||
|
||||
it 'can mark features as seen for a given user' do
|
||||
expect(DiscourseUpdates.unseen_new_features(admin.id)).to be_present
|
||||
|
||||
DiscourseUpdates.mark_new_features_as_seen(admin.id)
|
||||
expect(DiscourseUpdates.unseen_new_features(admin.id)).to be_empty
|
||||
|
||||
# doesn't affect another user
|
||||
expect(DiscourseUpdates.unseen_new_features(admin2.id)).to be_present
|
||||
|
||||
end
|
||||
|
||||
it 'correctly sees newly added features as unseen' do
|
||||
DiscourseUpdates.mark_new_features_as_seen(admin.id)
|
||||
expect(DiscourseUpdates.unseen_new_features(admin.id)).to be_empty
|
||||
expect(DiscourseUpdates.new_features_last_seen(admin.id)).to be_within(1.second).of (last_item_date)
|
||||
|
||||
updated_features = [
|
||||
{ "emoji" => "🤾", "title" => "Brand New Item", "created_at" => 2.minutes.ago }
|
||||
]
|
||||
updated_features += sample_features
|
||||
|
||||
Discourse.redis.set('new_features', MultiJson.dump(updated_features))
|
||||
|
||||
result = DiscourseUpdates.unseen_new_features(admin.id)
|
||||
expect(result.length).to eq(1)
|
||||
expect(result[0]["title"]).to eq("Brand New Item")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,6 +15,15 @@ describe Admin::DashboardController do
|
|||
context 'while logged in as an admin' do
|
||||
fab!(:admin) { Fabricate(:admin) }
|
||||
|
||||
def populate_new_features
|
||||
sample_features = [
|
||||
{ "id" => "1", "emoji" => "🤾", "title" => "Cool Beans", "description" => "Now beans are included", "created_at" => Time.zone.now - 40.minutes },
|
||||
{ "id" => "2", "emoji" => "🙈", "title" => "Fancy Legumes", "description" => "Legumes too!", "created_at" => Time.zone.now - 20.minutes }
|
||||
]
|
||||
|
||||
Discourse.redis.set('new_features', MultiJson.dump(sample_features))
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
@ -77,5 +86,49 @@ describe Admin::DashboardController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#new_features' do
|
||||
before do
|
||||
Discourse.redis.del "new_features_last_seen_user_#{admin.id}"
|
||||
Discourse.redis.del "new_features"
|
||||
end
|
||||
|
||||
it 'is empty by default' do
|
||||
get "/admin/dashboard/new-features.json"
|
||||
expect(response.status).to eq(200)
|
||||
json = response.parsed_body
|
||||
expect(json['new_features']).to eq(nil)
|
||||
end
|
||||
|
||||
it 'fails gracefully for invalid JSON' do
|
||||
Discourse.redis.set("new_features", "INVALID JSON")
|
||||
get "/admin/dashboard/new-features.json"
|
||||
expect(response.status).to eq(200)
|
||||
json = response.parsed_body
|
||||
expect(json['new_features']).to eq(nil)
|
||||
end
|
||||
|
||||
it 'includes new features when available' do
|
||||
populate_new_features
|
||||
|
||||
get "/admin/dashboard/new-features.json"
|
||||
expect(response.status).to eq(200)
|
||||
json = response.parsed_body
|
||||
|
||||
expect(json['new_features'].length).to eq(2)
|
||||
expect(json['new_features'][0]["emoji"]).to eq("🙈")
|
||||
expect(json['new_features'][0]["title"]).to eq("Fancy Legumes")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mark_new_features_as_seen' do
|
||||
it 'resets last seen for a given user' do
|
||||
populate_new_features
|
||||
put "/admin/dashboard/mark-new-features-as-seen.json"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(DiscourseUpdates.new_features_last_seen(admin.id)).not_to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue