FEATURE: Add new features section in admin dashboard (#11731)

This commit is contained in:
Penar Musaraj 2021-01-22 10:09:02 -05:00 committed by GitHub
parent 71656d2c37
commit 4f01ca87e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 327 additions and 0 deletions

View File

@ -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));
},
});

View File

@ -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;
});
},

View File

@ -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>

View File

@ -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}}

View File

@ -1,3 +1,5 @@
{{dashboard-new-features}}
{{plugin-outlet name="admin-dashboard-top"}}
{{#if showVersionChecks}}

View File

@ -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) {

View File

@ -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",
},
};

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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."

View File

@ -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

View File

@ -2134,6 +2134,10 @@ uncategorized:
client: true
hidden: true
check_for_new_features:
default: false
hidden: true
automatically_unpin_topics:
default: true
client: true

View File

@ -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

View File

@ -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

View File

@ -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