1
0
Fork 0

Merge pull request #157 from paviliondev/add_notice_inbox

Add notice inbox
Dieser Commit ist enthalten in:
Angus McLeod 2021-11-17 21:04:52 +08:00 committet von GitHub
Commit 75d881de06
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 4AEE18F83AFDEB23
34 geänderte Dateien mit 1052 neuen und 364 gelöschten Zeilen

Datei anzeigen

@ -0,0 +1,19 @@
import Component from "@ember/component";
import NoticeMessage from "../mixins/notice-message";
export default Component.extend(NoticeMessage, {
tagName: "tr",
attributeBindings: ["notice.id:data-notice-id"],
classNameBindings: [
":wizard-notice-row",
"notice.typeClass",
"notice.expired:expired",
"notice.dismissed:dismissed",
],
actions: {
dismiss() {
this.notice.dismiss();
},
},
});

Datei anzeigen

@ -1,35 +1,15 @@
import Component from "@ember/component"; import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators"; import NoticeMessage from "../mixins/notice-message";
import { not, notEmpty } from "@ember/object/computed";
import I18n from "I18n";
export default Component.extend({ export default Component.extend(NoticeMessage, {
attributeBindings: ["notice.id:data-notice-id"],
classNameBindings: [ classNameBindings: [
":wizard-notice", ":wizard-notice",
"notice.type", "notice.typeClass",
"dismissed", "notice.dismissed:dismissed",
"expired", "notice.expired:expired",
"resolved", "notice.hidden:hidden",
], ],
showFull: false,
resolved: notEmpty("notice.expired_at"),
dismissed: notEmpty("notice.dismissed_at"),
canDismiss: not("dismissed"),
@discourseComputed("notice.type")
title(type) {
return I18n.t(`admin.wizard.notice.title.${type}`);
},
@discourseComputed("notice.type")
icon(type) {
return {
plugin_status_warning: "exclamation-circle",
plugin_status_connection_error: "bolt",
subscription_messages_connection_error: "bolt",
info: "info-circle",
}[type];
},
actions: { actions: {
dismiss() { dismiss() {
@ -38,5 +18,12 @@ export default Component.extend({
this.set("dismissing", false); this.set("dismissing", false);
}); });
}, },
hide() {
this.set("hiding", true);
this.notice.hide().then(() => {
this.set("hiding", false);
});
},
}, },
}); });

Datei anzeigen

@ -0,0 +1,5 @@
{{#if notices}}
{{#each notices as |notice|}}
{{wizard-notice notice=notice showPlugin=true}}
{{/each}}
{{/if}}

Datei anzeigen

@ -0,0 +1,19 @@
import { getOwner } from "discourse-common/lib/get-owner";
export default {
shouldRender(attrs, ctx) {
return ctx.siteSettings.wizard_critical_notices_on_dashboard;
},
setupComponent(attrs, component) {
const controller = getOwner(this).lookup("controller:admin-dashboard");
component.set("notices", controller.get("customWizardCriticalNotices"));
controller.addObserver("customWizardCriticalNotices.[]", () => {
if (this._state === "destroying") {
return;
}
component.set("notices", controller.get("customWizardCriticalNotices"));
});
},
};

Datei anzeigen

@ -1,3 +0,0 @@
{{#if importantNotice}}
{{wizard-notice notice=importantNotice importantOnDashboard=true}}
{{/if}}

Datei anzeigen

@ -1,16 +0,0 @@
import { getOwner } from "discourse-common/lib/get-owner";
export default {
shouldRender(attrs, ctx) {
return ctx.siteSettings.wizard_important_notices_on_dashboard;
},
setupComponent() {
const controller = getOwner(this).lookup("controller:admin-dashboard");
const importantNotice = controller.get("customWizardImportantNotice");
if (importantNotice) {
this.set("importantNotice", importantNotice);
}
},
};

Datei anzeigen

@ -0,0 +1,68 @@
import Controller from "@ember/controller";
import CustomWizardNotice from "../models/custom-wizard-notice";
import discourseComputed from "discourse-common/utils/decorators";
import { notEmpty } from "@ember/object/computed";
import { A } from "@ember/array";
import I18n from "I18n";
export default Controller.extend({
messageUrl: "https://thepavilion.io/t/3652",
messageKey: "info",
messageIcon: "info-circle",
messageClass: "info",
hasNotices: notEmpty("notices"),
page: 0,
loadingMore: false,
canLoadMore: true,
@discourseComputed("notices.[]", "notices.@each.dismissed")
allDismisssed(notices) {
return notices.every((n) => !n.canDismiss || n.dismissed);
},
loadMoreNotices() {
if (!this.canLoadMore) {
return;
}
const page = this.get("page");
this.set("loadingMore", true);
CustomWizardNotice.list({ page, include_all: true })
.then((result) => {
if (result.notices.length === 0) {
this.set("canLoadMore", false);
return;
}
this.get("notices").pushObjects(
A(result.notices.map((notice) => CustomWizardNotice.create(notice)))
);
})
.finally(() => this.set("loadingMore", false));
},
actions: {
loadMore() {
if (this.canLoadMore) {
this.set("page", this.page + 1);
this.loadMoreNotices();
}
},
dismissAll() {
bootbox.confirm(
I18n.t("admin.wizard.notice.dismiss_all.confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
(result) => {
if (result) {
this.set("loadingMore", true);
CustomWizardNotice.dismissAll().finally(() =>
this.set("loadingMore", false)
);
}
}
);
},
},
});

Datei anzeigen

@ -1,20 +1,26 @@
import Controller from "@ember/controller"; import Controller, { inject as controller } from "@ember/controller";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { isPresent } from "@ember/utils";
import { ajax } from "discourse/lib/ajax"; import { A } from "@ember/array";
export default Controller.extend({ export default Controller.extend({
actions: { adminWizardsNotices: controller(),
dismissNotice(noticeId) {
ajax(`/admin/wizards/notice/${this.id}`, { unsubscribe() {
type: "DELETE", this.messageBus.unsubscribe("/custom-wizard/notices");
}) },
.then((result) => {
if (result.success) { subscribe() {
const notices = this.notices; this.unsubscribe();
notices.removeObject(notices.findBy("id", noticeId)); this.messageBus.subscribe("/custom-wizard/notices", (data) => {
} if (isPresent(data.active_notice_count)) {
}) this.set("activeNoticeCount", data.active_notice_count);
.catch(popupAjaxError); this.adminWizardsNotices.setProperties({
}, notices: A(),
page: 0,
canLoadMore: true,
});
this.adminWizardsNotices.loadMoreNotices();
}
});
}, },
}); });

Datei anzeigen

@ -63,6 +63,11 @@ export default {
path: "/subscription", path: "/subscription",
resetNamespace: true, resetNamespace: true,
}); });
this.route("adminWizardsNotices", {
path: "/notices",
resetNamespace: true,
});
} }
); );
}, },

Datei anzeigen

@ -0,0 +1,43 @@
import { autoUpdatingRelativeAge } from "discourse/lib/formatter";
import { iconHTML } from "discourse-common/lib/icon-library";
import I18n from "I18n";
import { registerUnbound } from "discourse-common/lib/helpers";
import { htmlSafe } from "@ember/template";
registerUnbound("notice-badge", function (attrs) {
let tag = attrs.url ? "a" : "div";
let attrStr = "";
if (attrs.title) {
attrStr += `title='${I18n.t(attrs.title)}'`;
}
if (attrs.url) {
attrStr += `href='${attrs.url}'`;
}
let html = `<${tag} class="${
attrs.class ? `${attrs.class} ` : ""
}notice-badge" ${attrStr}>`;
if (attrs.icon) {
html += iconHTML(attrs.icon);
}
if (attrs.label) {
if (attrs.icon) {
html += "&nbsp;";
}
html += `<span>${I18n.t(attrs.label)}</span>`;
}
if (attrs.date) {
if (attrs.icon || attrs.label) {
html += "&nbsp;";
}
let dateAttrs = {};
if (attrs.leaveAgo) {
dateAttrs = {
format: "medium",
leaveAgo: true,
};
}
html += autoUpdatingRelativeAge(new Date(attrs.date), dateAttrs);
}
html += `</${tag}>`;
return htmlSafe(html);
});

Datei anzeigen

@ -1,6 +1,7 @@
import DiscourseURL from "discourse/lib/url"; import DiscourseURL from "discourse/lib/url";
import { withPluginApi } from "discourse/lib/plugin-api"; import { withPluginApi } from "discourse/lib/plugin-api";
import CustomWizardNotice from "../models/custom-wizard-notice"; import CustomWizardNotice from "../models/custom-wizard-notice";
import { isPresent } from "@ember/utils";
import { A } from "@ember/array"; import { A } from "@ember/array";
export default { export default {
@ -22,35 +23,43 @@ export default {
withPluginApi("0.8.36", (api) => { withPluginApi("0.8.36", (api) => {
api.modifyClass("route:admin-dashboard", { api.modifyClass("route:admin-dashboard", {
afterModel() { setupController(controller) {
return CustomWizardNotice.list().then((result) => { this._super(...arguments);
if (result && result.length) {
this.set( controller.loadCriticalNotices();
"notices", controller.subscribe();
A(result.map((n) => CustomWizardNotice.create(n))) },
); });
api.modifyClass("controller:admin-dashboard", {
criticalNotices: A(),
unsubscribe() {
this.messageBus.unsubscribe("/custom-wizard/notices");
},
subscribe() {
this.unsubscribe();
this.messageBus.subscribe("/custom-wizard/notices", (data) => {
if (isPresent(data.active_notice_count)) {
this.loadCriticalNotices();
} }
}); });
}, },
setupController(controller) { loadCriticalNotices() {
if (this.notices) { CustomWizardNotice.list({
let pluginStatusConnectionError = this.notices.filter( type: ["connection_error", "warning"],
(n) => n.type === "plugin_status_connection_error" archetype: "plugin_status",
)[0]; visible: true,
let pluginStatusWarning = this.notices.filter( }).then((result) => {
(n) => n.type === "plugin_status_warning" if (result.notices && result.notices.length) {
)[0]; const criticalNotices = A(
result.notices.map((n) => CustomWizardNotice.create(n))
if (pluginStatusConnectionError || pluginStatusWarning) {
controller.set(
"customWizardImportantNotice",
pluginStatusConnectionError || pluginStatusWarning
); );
this.set("customWizardCriticalNotices", criticalNotices);
} }
} });
this._super(...arguments);
}, },
}); });
}); });

Datei anzeigen

@ -0,0 +1,68 @@
import Mixin from "@ember/object/mixin";
import { bind, scheduleOnce } from "@ember/runloop";
import { cookAsync } from "discourse/lib/text";
import { createPopper } from "@popperjs/core";
export default Mixin.create({
showCookedMessage: false,
didReceiveAttrs() {
const message = this.notice.message;
cookAsync(message).then((cooked) => {
this.set("cookedMessage", cooked);
});
},
createMessageModal() {
let container = this.element.querySelector(".notice-message");
let modal = this.element.querySelector(".cooked-notice-message");
this._popper = createPopper(container, modal, {
strategy: "absolute",
placement: "bottom-start",
modifiers: [
{
name: "preventOverflow",
},
{
name: "offset",
options: {
offset: [0, 5],
},
},
],
});
},
didInsertElement() {
$(document).on("click", bind(this, this.documentClick));
},
willDestroyElement() {
$(document).off("click", bind(this, this.documentClick));
},
documentClick(event) {
if (this._state === "destroying") {
return;
}
if (
!event.target.closest(
`[data-notice-id="${this.notice.id}"] .notice-message`
)
) {
this.set("showCookedMessage", false);
}
},
actions: {
toggleCookedMessage() {
this.toggleProperty("showCookedMessage");
if (this.showCookedMessage) {
scheduleOnce("afterRender", this, this.createMessageModal);
}
},
},
});

Datei anzeigen

@ -1,12 +1,38 @@
import EmberObject from "@ember/object"; import EmberObject from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { and, not, notEmpty } from "@ember/object/computed";
import { dasherize } from "@ember/string";
import I18n from "I18n";
const CustomWizardNotice = EmberObject.extend(); const CustomWizardNotice = EmberObject.extend({
expired: notEmpty("expired_at"),
dismissed: notEmpty("dismissed_at"),
hidden: notEmpty("hidden_at"),
notHidden: not("hidden"),
notDismissed: not("dismissed"),
canDismiss: and("dismissable", "notDismissed"),
canHide: and("can_hide", "notHidden"),
@discourseComputed("type")
typeClass(type) {
return dasherize(type);
},
@discourseComputed("type")
typeLabel(type) {
return I18n.t(`admin.wizard.notice.type.${type}`);
},
CustomWizardNotice.reopen({
dismiss() { dismiss() {
return ajax(`/admin/wizards/notice/${this.id}`, { type: "PUT" }) if (!this.get("canDismiss")) {
return;
}
return ajax(`/admin/wizards/notice/${this.get("id")}/dismiss`, {
type: "PUT",
})
.then((result) => { .then((result) => {
if (result.success) { if (result.success) {
this.set("dismissed_at", result.dismissed_at); this.set("dismissed_at", result.dismissed_at);
@ -14,11 +40,34 @@ CustomWizardNotice.reopen({
}) })
.catch(popupAjaxError); .catch(popupAjaxError);
}, },
hide() {
if (!this.get("canHide")) {
return;
}
return ajax(`/admin/wizards/notice/${this.get("id")}/hide`, { type: "PUT" })
.then((result) => {
if (result.success) {
this.set("hidden_at", result.hidden_at);
}
})
.catch(popupAjaxError);
},
}); });
CustomWizardNotice.reopenClass({ CustomWizardNotice.reopenClass({
list() { list(data = {}) {
return ajax("/admin/wizards/notice").catch(popupAjaxError); return ajax("/admin/wizards/notice", {
type: "GET",
data,
}).catch(popupAjaxError);
},
dismissAll() {
return ajax("/admin/wizards/notice/dismiss", {
type: "PUT",
}).catch(popupAjaxError);
}, },
}); });

Datei anzeigen

@ -0,0 +1,17 @@
import CustomWizardNotice from "../models/custom-wizard-notice";
import DiscourseRoute from "discourse/routes/discourse";
import { A } from "@ember/array";
export default DiscourseRoute.extend({
model() {
return CustomWizardNotice.list({ include_all: true });
},
setupController(controller, model) {
controller.setProperties({
notices: A(
model.notices.map((notice) => CustomWizardNotice.create(notice))
),
});
},
});

Datei anzeigen

@ -1,6 +1,5 @@
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { A } from "@ember/array";
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
model() { model() {
@ -8,8 +7,16 @@ export default DiscourseRoute.extend({
}, },
setupController(controller, model) { setupController(controller, model) {
controller.set("notices", A(model.notices));
controller.set("api_section", model.api_section); controller.set("api_section", model.api_section);
if (model.active_notice_count) {
controller.set("activeNoticeCount", model.active_notice_count);
}
if (model.featured_notices) {
controller.set("featuredNotices", model.featured_notices);
}
controller.subscribe();
}, },
afterModel(model, transition) { afterModel(model, transition) {

Datei anzeigen

@ -0,0 +1,49 @@
<div class="admin-wizard-controls">
<h3>{{i18n "admin.wizard.notices.title"}}</h3>
<div class="buttons">
{{d-button
label="admin.wizard.notice.dismiss_all.label"
title="admin.wizard.notice.dismiss_all.title"
action=(action "dismissAll")
disabled=allDismisssed
icon="check"}}
</div>
</div>
{{wizard-message
key=messageKey
url=messageUrl
type=messageType
opts=messageOpts
items=messageItems
loading=loading
component="notices"}}
<div class="wizard-table">
{{#load-more selector=".wizard-table tr" action=(action "loadMore")}}
{{#if hasNotices}}
<table>
<thead>
<tr>
<th>{{I18n "admin.wizard.notice.time"}}</th>
<th>{{I18n "admin.wizard.notice.type.label"}}</th>
<th>{{I18n "admin.wizard.notice.title"}}</th>
<th>{{I18n "admin.wizard.notice.status"}}</th>
</tr>
</thead>
<tbody>
{{#each notices as |notice|}}
{{wizard-notice-row notice=notice}}
{{/each}}
</tbody>
</table>
{{else}}
{{#unless loadingMore}}
<p>{{i18n "search.no_results"}}</p>
{{/unless}}
{{/if}}
{{conditional-loading-spinner condition=loadingMore}}
{{/load-more}}
</div>

Datei anzeigen

@ -10,6 +10,12 @@
{{nav-item route="adminWizardsSubscription" label="admin.wizard.subscription.nav_label"}} {{nav-item route="adminWizardsSubscription" label="admin.wizard.subscription.nav_label"}}
<div class="admin-actions"> <div class="admin-actions">
<div class="wizard-notices-link">
{{nav-item tagName="div" route="adminWizardsNotices" label="admin.wizard.notices.nav_label"}}
{{#if activeNoticeCount}}
<span class="active-notice-count">{{activeNoticeCount}}</span>
{{/if}}
</div>
<a target="_blank" class="btn btn-pavilion-support" rel="noreferrer noopener" href="https://thepavilion.io/w/support" title={{i18n "admin.wizard.support_button.title"}}> <a target="_blank" class="btn btn-pavilion-support" rel="noreferrer noopener" href="https://thepavilion.io/w/support" title={{i18n "admin.wizard.support_button.title"}}>
{{d-icon "far-life-ring"}}{{i18n "admin.wizard.support_button.label"}} {{d-icon "far-life-ring"}}{{i18n "admin.wizard.support_button.label"}}
</a> </a>
@ -17,8 +23,5 @@
{{/admin-nav}} {{/admin-nav}}
<div class="admin-container"> <div class="admin-container">
{{#each notices as |notice|}}
{{wizard-notice notice=notice}}
{{/each}}
{{outlet}} {{outlet}}
</div> </div>

Datei anzeigen

@ -0,0 +1,30 @@
<td class="small">
{{#if notice.updated_at}}
{{notice-badge class="notice-updated-at" date=notice.updated_at label="admin.wizard.notice.updated_at" leaveAgo=true}}
{{else}}
{{notice-badge class="notice-created-at" date=notice.created_at label="admin.wizard.notice.created_at" leaveAgo=true}}
{{/if}}
</td>
<td>{{notice.typeLabel}}</td>
<td class="notice-message">
<a role="button" {{action "toggleCookedMessage"}} class="show-notice-message">{{notice.title}}</a>
{{#if showCookedMessage}}
<span class="cooked-notice-message">{{cookedMessage}}</span>
{{/if}}
</td>
<td>
{{#if notice.canDismiss}}
{{d-button
action="dismiss"
label="admin.wizard.notice.dismiss.label"
title="admin.wizard.notice.dismiss.title"
class="btn-dismiss"
icon="check"}}
{{else if notice.dismissed}}
<span>{{i18n "admin.wizard.notice.dismissed_at"}}&nbsp;{{format-date notice.dismissed_at leaveAgo="true"}}</span>
{{else if notice.expired}}
<span>{{i18n "admin.wizard.notice.expired_at"}}&nbsp;{{format-date notice.expired_at leaveAgo="true"}}</span>
{{else}}
<span>{{i18n "admin.wizard.notice.active"}}</span>
{{/if}}
</td>

Datei anzeigen

@ -1,51 +1,39 @@
<div class="notice-header"> <div class="notice-header">
{{#if resolved}} <div class="notice-title notice-badge notice-message">
<div class="notice-expired-at notice-badge" title={{notice.expired_at}}> <a role="button" {{action "toggleCookedMessage"}} class="show-notice-message">{{notice.title}}</a>
{{d-icon "check"}} {{#if showCookedMessage}}
<span class="notice-resolved">{{i18n "admin.wizard.notice.resolved"}}</span> <span class="cooked-notice-message">{{cookedMessage}}</span>
{{format-date notice.expired_at leaveAgo="true"}} {{/if}}
</div>
{{/if}}
<div class="notice-title notice-badge" title={{title}}>
{{d-icon icon}}
<span>{{title}}</span>
</div> </div>
<div class="notice-header-right">
{{#if notice.expired}}
{{notice-badge class="notice-expired-at" icon="check" label="admin.wizard.notice.expired_at" date=notice.expired_at}}
{{/if}}
{{#if showPlugin}}
{{notice-badge class="notice-plugin" icon="plug" title="admin.wizard.notice.plugin" label="admin.wizard.notice.plugin" url="/admin/wizards/notices"}}
{{/if}}
{{notice-badge class="notice-created-at" icon="far-clock" label="admin.wizard.notice.created_at" date=notice.created_at leaveAgo=true}}
{{#if notice.updated_at}}
{{notice-badge class="notice-updated-at" icon="far-clock" label="admin.wizard.notice.updated_at" date=notice.updated_at}}
{{/if}}
<div class="notice-created-at notice-badge" title={{notice.created_at}}> {{#if notice.canDismiss}}
{{d-icon "far-clock"}} <div class="dismiss-notice-container">
<span class="notice-issued">{{i18n "admin.wizard.notice.issued"}}</span> {{#if dismissing}}
{{format-date notice.created_at leaveAgo="true"}} {{loading-spinner size="small"}}
</div> {{else}}
<a {{action "dismiss"}} role="button" class="dismiss-notice">{{d-icon "times"}}</a>
{{#if notice.updated_at}} {{/if}}
<div class="notice-updated-at notice-badge" title={{notice.updated_at}}> </div>
{{d-icon "calendar-alt"}} {{/if}}
<span class="notice-updated">{{i18n "admin.wizard.notice.updated"}}</span> {{#if notice.canHide}}
{{format-date notice.updated_at leaveAgo="true"}} <div class="hide-notice-container">
</div> {{#if hiding}}
{{/if}} {{loading-spinner size="small"}}
{{else}}
<div class="notice-plugin notice-badge" title={{i18n "admin.wizard.notice.plugin"}}> <a {{action "hide"}} role="button" class="hide-notice">{{d-icon "far-eye-slash"}}</a>
{{d-icon "plug"}} {{/if}}
<span>{{i18n "admin.wizard.notice.plugin"}}</span> </div>
{{/if}}
</div> </div>
</div> </div>
<div class="notice-message">
{{html-safe notice.message}}
</div>
{{#if importantOnDashboard}}
<a href="/admin/site_settings/category/all_results?filter=wizard_important_notices_on_dashboard" class="disable-important">
{{i18n "admin.wizard.notice.disable_important_on_dashboard"}}
</a>
{{/if}}
{{#if canDismiss}}
{{#if dismissing}}
{{loading-spinner size="small"}}
{{else}}
<a {{action "dismiss"}} role="button" class="dismiss-notice">{{d-icon "times"}}</a>
{{/if}}
{{/if}}

Datei anzeigen

@ -4,15 +4,25 @@
@import "common/components/buttons"; @import "common/components/buttons";
@import "wizard/variables"; @import "wizard/variables";
$expired: #339b18;
$info: #038ae7;
$warning: #d47e00;
$error: #ef1700;
.admin-wizard-controls { .admin-wizard-controls {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 10px; margin-bottom: 10px;
min-height: 34px;
& + .wizard-message + div { & + .wizard-message + div {
margin-top: 20px; margin-top: 20px;
} }
h3 {
margin-bottom: 0;
}
} }
.wizard-message { .wizard-message {
@ -909,87 +919,142 @@
} }
} }
.admin-wizards-notices {
.wizard-table {
overflow: unset;
}
}
.wizard-notice { .wizard-notice {
padding: 1em; padding: 0.75em;
margin-bottom: 1em; margin-bottom: 1em;
border: 1px solid var(--primary); border: 1px solid var(--primary-low);
position: relative;
&.dismissed { &.dismissed {
display: none; display: none;
} }
&.resolved .notice-badge:not(.notice-expired-at), &.expired .notice-badge:not(.notice-expired-at),
&.resolved a, &.expired a,
&.resolved p { &.expired p {
color: var(--primary-medium) !important; color: var(--primary-medium) !important;
} }
.d-icon { .notice-badge {
margin-right: 0.4em; padding: 0 0.5em;
} }
.notice-header { .notice-header {
display: flex; display: flex;
}
.notice-badge {
border: 1px solid var(--primary);
display: inline-flex;
align-items: center;
padding: 0 0.5em;
margin-right: 1em;
font-size: 0.9em;
line-height: 25px;
min-height: 25px;
box-sizing: border-box;
&:last-of-type {
margin-right: 0;
}
}
&.warning {
.notice-expired-at {
border: 1px solid var(--success);
background-color: rgba($success, 0.1);
color: var(--success);
}
.notice-title { .notice-title {
border: 1px solid var(--pavilion-warning); padding: 0;
background-color: rgba($pavilion_warning, 0.1); }
color: var(--pavilion-warning);
.notice-header-right {
margin-left: auto;
display: flex;
align-items: center;
.notice-badge {
margin-left: 0.5em;
}
} }
} }
.notice-issued, .dismiss-notice-container,
.notice-resolved { .hide-notice-container {
margin-right: 0.3em; width: 40px;
} display: flex;
justify-content: center;
.notice-message { align-items: center;
p {
margin: 0.5em 0;
}
p:last-of-type {
margin-bottom: 0;
}
} }
.dismiss-notice, .dismiss-notice,
.spinner { .hide-notice {
position: absolute; display: flex;
top: 1em; align-items: center;
right: 1em;
color: var(--primary-medium);
}
.disable-important { .d-icon {
position: absolute; margin-right: 0;
right: 3em; color: var(--primary);
top: 1em; }
color: var(--primary-medium); }
}
.notice-badge {
display: inline-flex;
align-items: center;
min-height: 25px;
box-sizing: border-box;
color: var(--primary) !important;
}
.admin-actions {
display: flex;
align-items: center;
}
.wizard-notices-link {
position: relative;
margin-right: 10px;
div > a {
@include btn;
color: var(--secondary) !important;
background-color: var(--primary-medium);
&.active {
background-color: var(--tertiary) !important;
color: var(--secondary) !important;
}
}
}
.active-notice-count {
background-color: $danger;
color: $secondary;
border-radius: 50%;
width: 18px;
height: 18px;
line-height: 18px;
text-align: center;
position: absolute;
top: -8px;
right: -8px;
font-size: 0.7em;
}
a.show-notice-message {
padding: 0.25em 0.5em;
color: var(--primary);
}
.wizard-notice,
.wizard-notice-row:not(.expired):not(.dismissed) {
&.info {
background-color: rgba($info, 0.1);
border: 1px solid rgba($info, 0.5);
}
&.warning,
&.connection-error {
background-color: rgba($warning, 0.1);
border: 1px solid rgba($warning, 0.5);
}
}
.notice-message {
position: relative;
.cooked-notice-message {
background-color: var(--secondary);
padding: 1em;
z-index: 1;
box-shadow: shadow("dropdown");
border-top: 1px solid var(--primary-low);
p {
margin: 0;
}
} }
} }

Datei anzeigen

@ -2,10 +2,6 @@
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
h3 {
margin-bottom: 0;
}
.buttons { .buttons {
display: flex; display: flex;
margin-left: auto; margin-left: auto;

Datei anzeigen

@ -112,6 +112,9 @@ en:
select: "Select a wizard to see its logs" select: "Select a wizard to see its logs"
viewing: "View recent logs for wizards on the forum" viewing: "View recent logs for wizards on the forum"
documentation: "Check out the logs documentation" documentation: "Check out the logs documentation"
notices:
info: "Plugin status and subscription notices"
documentation: Check out the notices documentation
editor: editor:
show: "Show" show: "Show"
@ -485,14 +488,31 @@ en:
notice: notice:
plugin: Custom Wizard Plugin plugin: Custom Wizard Plugin
issued: Issued message: Message
update: Updated time: Time
resolved: Resolved status: Status
title: title: Title
plugin_status_warning: Warning Notice dismiss:
plugin_status_connection_error: Connection Notice label: Dismiss
subscription_messages_connection_error: Connection Notice title: Dismiss notice
disable_important_on_dashboard: disable dismiss_all:
label: Dismiss All
title: Dismiss all informational Custom Wizard notices
confirm: Are you sure you want to dismiss all informational Custom Wizard notices?
active: active
created_at: issued
updated_at: updated
expired_at: expired
dismissed_at: dismissed
type:
label: Type
info: Information
warning: Warning
connection_error: Connection Error
notices:
nav_label: Notices
title: Plugin Notices
wizard_js: wizard_js:
group: group:

Datei anzeigen

@ -54,21 +54,21 @@ en:
notice: notice:
connection_error: "Failed to connect to http://%{domain}" connection_error: "Failed to connect to http://%{domain}"
compatibility_issue: > compatibility_issue:
The Custom Wizard Plugin has a compatibility issue with the latest version of Discourse. title: The Custom Wizard Plugin is incompatibile with the latest version of Discourse.
Please check the Custom Wizard Plugin status on [%{domain}](http://%{domain}) before updating Discourse. message: Please check the Custom Wizard Plugin status on [%{domain}](http://%{domain}) before updating Discourse.
plugin_status: plugin_status:
connection_error_limit: > connection_error:
We're unable to connect to the Pavilion Plugin Status Server. Please check the Custom Wizard Plugin status on [%{domain}](http://%{domain}) before updating Discourse or the plugin. title: Unable to connect to the Custom Wizard Plugin status server
If this connection issue persists please contact <a href="mailto:support@thepavilion.io">support@thepavilion.io</a> for further assistance. message: Please check the Custom Wizard Plugin status on [%{domain}](http://%{domain}) before updating Discourse. If this issue persists contact <a href="mailto:support@thepavilion.io">support@thepavilion.io</a> for further assistance.
subscription_messages: subscription_message:
connection_error_limit: > connection_error:
We're unable to connect to the Pavilion Subscription Server. This will not affect the operation of the plugin. title: Unable to connect to the Custom Wizard Plugin subscription server
If this connection issue persists please contact <a href="mailto:support@thepavilion.io">support@thepavilion.io</a> for further assistance. message: If this issue persists contact <a href="mailto:support@thepavilion.io">support@thepavilion.io</a> for further assistance.
site_settings: site_settings:
custom_wizard_enabled: "Enable custom wizards." custom_wizard_enabled: "Enable custom wizards."
wizard_redirect_exclude_paths: "Routes excluded from wizard redirects." wizard_redirect_exclude_paths: "Routes excluded from wizard redirects."
wizard_recognised_image_upload_formats: "File types which will result in upload displaying an image preview" wizard_recognised_image_upload_formats: "File types which will result in upload displaying an image preview"
wizard_apis_enabled: "Enable API features (experimental)." wizard_apis_enabled: "Enable API features (experimental)."
wizard_important_notices_on_dashboard: "Show important notices about the custom wizard plugin on the admin dashboard." wizard_critical_notices_on_dashboard: "Show critical notices about the custom wizard plugin on the admin dashboard."

Datei anzeigen

@ -52,6 +52,9 @@ Discourse::Application.routes.append do
delete 'admin/wizards/subscription/authorize' => 'admin_subscription#destroy_authentication' delete 'admin/wizards/subscription/authorize' => 'admin_subscription#destroy_authentication'
get 'admin/wizards/notice' => 'admin_notice#index' get 'admin/wizards/notice' => 'admin_notice#index'
put 'admin/wizards/notice/:notice_id' => 'admin_notice#dismiss' put 'admin/wizards/notice/:notice_id/dismiss' => 'admin_notice#dismiss'
put 'admin/wizards/notice/:notice_id/hide' => 'admin_notice#hide'
put 'admin/wizards/notice/dismiss' => 'admin_notice#dismiss_all'
get 'admin/wizards/notices' => 'admin_notice#index'
end end
end end

Datei anzeigen

@ -6,8 +6,12 @@ class CustomWizard::AdminController < ::Admin::AdminController
render_json_dump( render_json_dump(
#TODO replace with appropriate static? #TODO replace with appropriate static?
api_section: ["business"].include?(CustomWizard::Subscription.type), api_section: ["business"].include?(CustomWizard::Subscription.type),
notices: ActiveModel::ArraySerializer.new( active_notice_count: CustomWizard::Notice.active_count,
CustomWizard::Notice.list, featured_notices: ActiveModel::ArraySerializer.new(
CustomWizard::Notice.list(
type: CustomWizard::Notice.types[:info],
archetype: CustomWizard::Notice.archetypes[:subscription_message]
),
each_serializer: CustomWizard::NoticeSerializer each_serializer: CustomWizard::NoticeSerializer
) )
) )

Datei anzeigen

@ -1,20 +1,66 @@
# frozen_string_literal: true # frozen_string_literal: true
class CustomWizard::AdminNoticeController < CustomWizard::AdminController class CustomWizard::AdminNoticeController < CustomWizard::AdminController
before_action :find_notice, only: [:dismiss] before_action :find_notice, only: [:dismiss, :hide]
def index def index
render_serialized(CustomWizard::Notice.list(include_recently_expired: true), CustomWizard::NoticeSerializer) type = params[:type]
archetype = params[:archtype]
page = params[:page].to_i
include_all = ActiveRecord::Type::Boolean.new.cast(params[:include_all])
visible = ActiveRecord::Type::Boolean.new.cast(params[:visible])
if type
if type.is_a?(Array)
type = type.map { |t| CustomWizard::Notice.types[t.to_sym] }
else
type = CustomWizard::Notice.types[type.to_sym]
end
end
if archetype
if archetype.is_a?(Array)
archetype = archetype.map { |t| CustomWizard::Notice.archetypes[archetype.to_sym] }
else
archetype = CustomWizard::Notice.archetypes[archetype.to_sym]
end
end
notices = CustomWizard::Notice.list(
include_all: include_all,
page: page,
type: type,
archetype: archetype,
visible: visible
)
render_serialized(notices, CustomWizard::NoticeSerializer, root: :notices)
end end
def dismiss def dismiss
if @notice.dismissable? && @notice.dismiss if @notice.dismissable? && @notice.dismiss!
render json: success_json.merge(dismissed_at: @notice.dismissed_at) render json: success_json.merge(dismissed_at: @notice.dismissed_at)
else else
render json: failed_json render json: failed_json
end end
end end
def hide
if @notice.can_hide? && @notice.hide!
render json: success_json.merge(hidden_at: @notice.hidden_at)
else
render json: failed_json
end
end
def dismiss_all
if CustomWizard::Notice.dismiss_all
render json: success_json
else
render json: failed_json
end
end
def find_notice def find_notice
@notice = CustomWizard::Notice.find(params[:notice_id]) @notice = CustomWizard::Notice.find(params[:notice_id])
raise Discourse::InvalidParameters.new(:notice_id) unless @notice raise Discourse::InvalidParameters.new(:notice_id) unless @notice

Datei anzeigen

@ -7,42 +7,67 @@ class CustomWizard::Notice
"tests-passed" => "plugins.discourse.pavilion.tech", "tests-passed" => "plugins.discourse.pavilion.tech",
"stable" => "stable.plugins.discourse.pavilion.tech" "stable" => "stable.plugins.discourse.pavilion.tech"
} }
SUBSCRIPTION_MESSAGES_DOMAIN = "test.thepavilion.io" SUBSCRIPTION_MESSAGE_DOMAIN = "test.thepavilion.io"
LOCALHOST_DOMAIN = "localhost:3000" LOCALHOST_DOMAIN = "localhost:3000"
PLUGIN_STATUSES_TO_WARN = %w(incompatible tests_failing) PLUGIN_STATUSES_TO_WARN = %w(incompatible tests_failing)
CHECK_PLUGIN_STATUS_ON_BRANCH = %w(tests-passed main stable) CHECK_PLUGIN_STATUS_ON_BRANCH = %w(tests-passed main stable)
PAGE_LIMIT = 30
attr_reader :id, attr_reader :id,
:title,
:message, :message,
:type, :type,
:archetype,
:created_at :created_at
attr_accessor :retrieved_at, attr_accessor :retrieved_at,
:updated_at, :updated_at,
:dismissed_at, :dismissed_at,
:expired_at :expired_at,
:hidden_at
def initialize(attrs) def initialize(attrs)
@id = Digest::SHA1.hexdigest(attrs[:message]) @id = self.class.generate_notice_id(attrs[:title], attrs[:created_at])
@title = attrs[:title]
@message = attrs[:message] @message = attrs[:message]
@type = attrs[:type].to_i @type = attrs[:type].to_i
@archetype = attrs[:archetype].to_i
@created_at = attrs[:created_at] @created_at = attrs[:created_at]
@updated_at = attrs[:updated_at] @updated_at = attrs[:updated_at]
@retrieved_at = attrs[:retrieved_at] @retrieved_at = attrs[:retrieved_at]
@dismissed_at = attrs[:dismissed_at] @dismissed_at = attrs[:dismissed_at]
@expired_at = attrs[:expired_at] @expired_at = attrs[:expired_at]
@hidden_at = attrs[:hidden_at]
end end
def dismiss def dismiss!
if dismissable? if dismissable?
self.dismissed_at = Time.now self.dismissed_at = Time.now
self.save self.save_and_publish
end end
end end
def expire def hide!
self.expired_at = Time.now if can_hide?
self.save self.hidden_at = Time.now
self.save_and_publish
end
end
def expire!
if !expired?
self.expired_at = Time.now
self.save_and_publish
end
end
def save_and_publish
if self.save
self.class.publish_notice_count
true
else
false
end
end end
def expired? def expired?
@ -54,19 +79,37 @@ class CustomWizard::Notice
end end
def dismissable? def dismissable?
true !expired? && !dismissed? && type === self.class.types[:info]
end
def hidden?
hidden_at.present?
end
def can_hide?
!hidden? && (
type === self.class.types[:connection_error] ||
type === self.class.types[:warning]
) && (
archetype === self.class.archetypes[:plugin_status]
)
end end
def save def save
attrs = { attrs = {
expired_at: expired_at, expired_at: expired_at,
updated_at: updated_at,
retrieved_at: retrieved_at,
created_at: created_at, created_at: created_at,
title: title,
message: message, message: message,
type: type type: type,
archetype: archetype
} }
if current = self.class.find(self.id) if current = self.class.find(self.id)
attrs[:dismissed_at] = current.dismissed_at || self.dismissed_at attrs[:dismissed_at] = current.dismissed_at || self.dismissed_at
attrs[:hidden_at] = current.hidden_at || self.hidden_at
end end
self.class.store(id, attrs) self.class.store(id, attrs)
@ -75,9 +118,15 @@ class CustomWizard::Notice
def self.types def self.types
@types ||= Enum.new( @types ||= Enum.new(
info: 0, info: 0,
plugin_status_warning: 1, warning: 1,
plugin_status_connection_error: 2, connection_error: 2
subscription_messages_connection_error: 3 )
end
def self.archetypes
@archetypes ||= Enum.new(
subscription_message: 0,
plugin_status: 1
) )
end end
@ -85,7 +134,7 @@ class CustomWizard::Notice
notices = [] notices = []
if !skip_subscription if !skip_subscription
subscription_messages = request(:subscription_messages) subscription_messages = request(:subscription_message)
if subscription_messages.present? if subscription_messages.present?
subscription_notices = convert_subscription_messages_to_notices(subscription_messages[:messages]) subscription_notices = convert_subscription_messages_to_notices(subscription_messages[:messages])
@ -96,27 +145,43 @@ class CustomWizard::Notice
if !skip_plugin && request_plugin_status? if !skip_plugin && request_plugin_status?
plugin_status = request(:plugin_status) plugin_status = request(:plugin_status)
if plugin_status.present? && plugin_status[:status].present? && plugin_status[:status].is_a?(Hash) if plugin_status.present? && plugin_status[:status].present?
plugin_notice = convert_plugin_status_to_notice(plugin_status[:status]) plugin_notice = convert_plugin_status_to_notice(plugin_status)
notices.push(plugin_notice) if plugin_notice notices.push(plugin_notice) if plugin_notice
end end
end end
notices.each do |notice_data| if notices.any?
notice = new(notice_data) notices.each do |notice_data|
notice.retrieved_at = Time.now notice = new(notice_data)
notice.save notice.retrieved_at = Time.now
notice.save
end
publish_notice_count
end end
end end
def self.publish_notice_count
payload = {
active_notice_count: CustomWizard::Notice.active_count
}
MessageBus.publish("/custom-wizard/notices", payload, group_ids: [Group::AUTO_GROUPS[:admins]])
end
def self.convert_subscription_messages_to_notices(messages) def self.convert_subscription_messages_to_notices(messages)
messages.map do |message| messages.reduce([]) do |result, message|
{ id = generate_notice_id(message[:title], message[:created_at])
result.push(
id: id,
title: message[:title],
message: message[:message], message: message[:message],
type: types[message[:type].to_sym], type: types[message[:type].to_sym],
archetype: archetypes[:subscription_message],
created_at: message[:created_at], created_at: message[:created_at],
expired_at: message[:expired_at] expired_at: message[:expired_at]
} )
result
end end
end end
@ -124,22 +189,32 @@ class CustomWizard::Notice
notice = nil notice = nil
if PLUGIN_STATUSES_TO_WARN.include?(plugin_status[:status]) if PLUGIN_STATUSES_TO_WARN.include?(plugin_status[:status])
notice = { title = I18n.t('wizard.notice.compatibility_issue.title')
message: I18n.t('wizard.notice.compatibility_issue', domain: plugin_status_domain), created_at = plugin_status[:status_changed_at]
type: types[:plugin_status_warning], id = generate_notice_id(title, created_at)
created_at: plugin_status[:status_changed_at]
} unless exists?(id)
message = I18n.t('wizard.notice.compatibility_issue.message', domain: plugin_status_domain)
notice = {
id: id,
title: title,
message: message,
type: types[:warning],
archetype: archetypes[:plugin_status],
created_at: created_at
}
end
else else
expire_notices(types[:plugin_status_warning]) expire_all(types[:warning], archetypes[:plugin_status])
end end
notice notice
end end
def self.notify_connection_errors(connection_type_key) def self.notify_connection_errors(archetype)
domain = self.send("#{connection_type_key.to_s}_domain") domain = self.send("#{archetype.to_s}_domain")
message = I18n.t("wizard.notice.#{connection_type_key.to_s}.connection_error_limit", domain: domain) title = I18n.t("wizard.notice.#{archetype.to_s}.connection_error.title")
notices = list(type: types[:connection_error], message: message) notices = list(type: types[:connection_error], archetype: archetypes[archetype.to_sym], title: title)
if notices.any? if notices.any?
notice = notices.first notice = notices.first
@ -147,28 +222,29 @@ class CustomWizard::Notice
notice.save notice.save
else else
notice = new( notice = new(
message: message, title: title,
type: types["#{connection_type_key}_connection_error".to_sym], message: I18n.t("wizard.notice.#{archetype.to_s}.connection_error.message", domain: domain),
created_at: Time.now archetype: archetypes[archetype.to_sym],
type: types[:connection_error],
created_at: Time.now,
updated_at: Time.now
) )
notice.save notice.save
end end
end
def self.expire_notices(type) publish_notice_count
list(type: type).each(&:expire)
end end
def self.request_plugin_status? def self.request_plugin_status?
CHECK_PLUGIN_STATUS_ON_BRANCH.include?(Discourse.git_branch) || Rails.env.test? || Rails.env.development? CHECK_PLUGIN_STATUS_ON_BRANCH.include?(Discourse.git_branch) || Rails.env.test? || Rails.env.development?
end end
def self.subscription_messages_domain def self.subscription_message_domain
(Rails.env.test? || Rails.env.development?) ? LOCALHOST_DOMAIN : SUBSCRIPTION_MESSAGES_DOMAIN (Rails.env.test? || Rails.env.development?) ? LOCALHOST_DOMAIN : SUBSCRIPTION_MESSAGE_DOMAIN
end end
def self.subscription_messages_url def self.subscription_message_url
"http://#{subscription_messages_domain}/subscription-server/messages.json" "http://#{subscription_message_domain}/subscription-server/messages.json"
end end
def self.plugin_status_domain def self.plugin_status_domain
@ -180,14 +256,19 @@ class CustomWizard::Notice
"http://#{plugin_status_domain}/plugin-manager/status/discourse-custom-wizard" "http://#{plugin_status_domain}/plugin-manager/status/discourse-custom-wizard"
end end
def self.request(type) def self.request(archetype)
url = self.send("#{type.to_s}_url") url = self.send("#{archetype.to_s}_url")
response = Excon.get(url)
connection_error = CustomWizard::Notice::ConnectionError.new(type)
if response.status == 200 begin
response = Excon.get(url)
rescue Excon::Error::Socket, Excon::Error::Timeout => e
response = nil
end
connection_error = CustomWizard::Notice::ConnectionError.new(archetype)
if response && response.status == 200
connection_error.expire! connection_error.expire!
expire_notices(types["#{type}_connection_error".to_sym]) expire_all(types[:connection_error], archetypes[archetype.to_sym])
begin begin
data = JSON.parse(response.body).deep_symbolize_keys data = JSON.parse(response.body).deep_symbolize_keys
@ -198,8 +279,7 @@ class CustomWizard::Notice
data data
else else
connection_error.create! connection_error.create!
notify_connection_errors(type) if connection_error.reached_limit? notify_connection_errors(archetype) if connection_error.reached_limit?
nil nil
end end
end end
@ -213,20 +293,65 @@ class CustomWizard::Notice
new(raw.symbolize_keys) if raw.present? new(raw.symbolize_keys) if raw.present?
end end
def self.exists?(id)
PluginStoreRow.where(plugin_name: namespace, key: id).exists?
end
def self.store(id, raw_notice) def self.store(id, raw_notice)
PluginStore.set(namespace, id, raw_notice) PluginStore.set(namespace, id, raw_notice)
end end
def self.list_query(type: nil, message: nil, include_recently_expired: false) def self.list_query(type: nil, archetype: nil, title: nil, include_all: false, page: nil, visible: false)
query = PluginStoreRow.where(plugin_name: namespace) query = PluginStoreRow.where(plugin_name: namespace)
query = query.where("(value::json->>'expired_at') IS NULL#{include_recently_expired ? " OR (value::json->>'expired_at')::date > now()::date - 1" : ""}") query = query.where("(value::json->>'hidden_at') IS NULL") if visible
query = query.where("(value::json->>'type')::integer = ?", type) if type query = query.where("(value::json->>'dismissed_at') IS NULL") unless include_all
query = query.where("(value::json->>'message')::text = ?", message) if message query = query.where("(value::json->>'expired_at') IS NULL") unless include_all
query.order("value::json->>'created_at' DESC") query = query.where("(value::json->>'archetype')::integer = ?", archetype) if archetype
if type
type_query_str = type.is_a?(Array) ? "(value::json->>'type')::integer IN (?)" : "(value::json->>'type')::integer = ?"
query = query.where(type_query_str, type)
end
query = query.where("(value::json->>'title')::text = ?", title) if title
query = query.limit(PAGE_LIMIT).offset(page.to_i * PAGE_LIMIT) if !page.nil?
query.order("value::json->>'expired_at' DESC, value::json->>'updated_at' DESC,value::json->>'dismissed_at' DESC, value::json->>'created_at' DESC")
end end
def self.list(type: nil, message: nil, include_recently_expired: false) def self.list(type: nil, archetype: nil, title: nil, include_all: false, page: 0, visible: false)
list_query(type: type, message: message, include_recently_expired: include_recently_expired) list_query(type: type, archetype: archetype, title: title, include_all: include_all, page: page, visible: visible)
.map { |r| self.new(JSON.parse(r.value).symbolize_keys) } .map { |r| self.new(JSON.parse(r.value).symbolize_keys) }
end end
def self.active_count
list_query.count
end
def self.dismiss_all
dismissed_count = PluginStoreRow.where("
plugin_name = '#{namespace}' AND
(value::json->>'type')::integer = #{types[:info]} AND
(value::json->>'expired_at') IS NULL AND
(value::json->>'dismissed_at') IS NULL
").update_all("
value = jsonb_set(value::jsonb, '{dismissed_at}', (to_json(now())::text)::jsonb, true)
")
publish_notice_count if dismissed_count.to_i > 0
dismissed_count
end
def self.expire_all(type, archetype)
expired_count = PluginStoreRow.where("
plugin_name = '#{namespace}' AND
(value::json->>'type')::integer = #{type} AND
(value::json->>'archetype')::integer = #{archetype} AND
(value::json->>'expired_at') IS NULL
").update_all("
value = jsonb_set(value::jsonb, '{expired_at}', (to_json(now())::text)::jsonb, true)
")
publish_notice_count if expired_count.to_i > 0
expired_count
end
def self.generate_notice_id(title, created_at)
Digest::SHA1.hexdigest("#{title}-#{created_at}")
end
end end

Datei anzeigen

@ -2,80 +2,76 @@
class CustomWizard::Notice::ConnectionError class CustomWizard::Notice::ConnectionError
attr_reader :type_key attr_reader :archetype
def initialize(type_key) def initialize(archetype)
@type_key = type_key @archetype = archetype
end end
def create! def create!
id = "#{type_key.to_s}_error" if attrs = current_error
key = "#{archetype.to_s}_error_#{attrs[:id]}"
if attrs = PluginStore.get(namespace, id) attrs[:updated_at] = Time.now
attrs['updated_at'] = Time.now attrs[:count] = attrs[:count].to_i + 1
attrs['count'] = attrs['count'].to_i + 1
else else
domain = CustomWizard::Notice.send("#{type_key.to_s}_domain") domain = CustomWizard::Notice.send("#{archetype.to_s}_domain")
id = SecureRandom.hex(8)
attrs = { attrs = {
id: id,
message: I18n.t("wizard.notice.connection_error", domain: domain), message: I18n.t("wizard.notice.connection_error", domain: domain),
type: self.class.types[type_key], archetype: CustomWizard::Notice.archetypes[archetype.to_sym],
created_at: Time.now, created_at: Time.now,
count: 1 count: 1
} }
key = "#{archetype.to_s}_error_#{id}"
end end
PluginStore.set(namespace, id, attrs) PluginStore.set(namespace, key, attrs)
@errors = nil
@current_error = nil
end end
def expire! def expire!
if errors.exists? if query = current_error(query_only: true)
errors.each do |error_row| record = query.first
error = JSON.parse(error_row.value) error = JSON.parse(record.value)
error['expired_at'] = Time.now error['expired_at'] = Time.now
error_row.value = error.to_json record.value = error.to_json
error_row.save record.save
end
end end
end end
def self.types
@types ||= Enum.new(
plugin_status: 0,
subscription_messages: 1
)
end
def plugin_status_limit def plugin_status_limit
5 5
end end
def subscription_messages_limit def subscription_message_limit
10 10
end end
def limit def limit
self.send("#{type_key.to_s}_limit") self.send("#{archetype.to_s}_limit")
end end
def reached_limit? def reached_limit?
return false unless errors.exists? return false unless current_error.present?
current_error['count'].to_i >= limit current_error[:count].to_i >= limit
end
def current_error
JSON.parse(errors.first.value)
end end
def namespace def namespace
"#{CustomWizard::PLUGIN_NAME}_notice_connection" "#{CustomWizard::PLUGIN_NAME}_notice_connection"
end end
def errors def current_error(query_only: false)
@errors ||= begin @current_error ||= begin
query = PluginStoreRow.where(plugin_name: namespace) query = PluginStoreRow.where(plugin_name: namespace)
query = query.where("(value::json->>'type')::integer = ?", self.class.types[type_key]) query = query.where("(value::json->>'archetype')::integer = ?", CustomWizard::Notice.archetypes[archetype.to_sym])
query.where("(value::json->>'expired_at') IS NULL") query = query.where("(value::json->>'expired_at') IS NULL")
return nil if !query.exists?
return query if query_only
JSON.parse(query.first.value).deep_symbolize_keys
end end
end end
end end

Datei anzeigen

@ -245,7 +245,10 @@ after_initialize do
end end
AdminDashboardData.add_problem_check do AdminDashboardData.add_problem_check do
warning_notices = CustomWizard::Notice.list(type: CustomWizard::Notice.types[:plugin_status_warning]) warning_notices = CustomWizard::Notice.list(
type: CustomWizard::Notice.types[:warning],
archetype: CustomWizard::Notice.archetypes[:plugin_status]
)
warning_notices.any? ? ActionView::Base.full_sanitizer.sanitize(warning_notices.first.message, tags: %w(a)) : nil warning_notices.any? ? ActionView::Base.full_sanitizer.sanitize(warning_notices.first.message, tags: %w(a)) : nil
end end

Datei anzeigen

@ -2,19 +2,27 @@
class CustomWizard::NoticeSerializer < ApplicationSerializer class CustomWizard::NoticeSerializer < ApplicationSerializer
attributes :id, attributes :id,
:title,
:message, :message,
:type, :type,
:archetype,
:created_at, :created_at,
:expired_at, :expired_at,
:updated_at, :updated_at,
:dismissed_at, :dismissed_at,
:retrieved_at, :retrieved_at,
:dismissable :hidden_at,
:dismissable,
:can_hide
def dismissable def dismissable
object.dismissable? object.dismissable?
end end
def can_hide
object.can_hide?
end
def type def type
CustomWizard::Notice.types.key(object.type) CustomWizard::Notice.types.key(object.type)
end end

Datei anzeigen

@ -6,6 +6,7 @@ describe CustomWizard::Notice do
fab!(:user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) }
let(:subscription_message) { let(:subscription_message) {
{ {
title: "Title of message about subscription",
message: "Message about subscription", message: "Message about subscription",
type: "info", type: "info",
created_at: Time.now - 3.day, created_at: Time.now - 3.day,
@ -23,7 +24,7 @@ describe CustomWizard::Notice do
context "subscription message" do context "subscription message" do
before do before do
freeze_time freeze_time
stub_request(:get, described_class.subscription_messages_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) stub_request(:get, described_class.subscription_message_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json)
described_class.update(skip_plugin: true) described_class.update(skip_plugin: true)
end end
@ -36,46 +37,73 @@ describe CustomWizard::Notice do
it "expires notice if subscription message is expired" do it "expires notice if subscription message is expired" do
subscription_message[:expired_at] = Time.now subscription_message[:expired_at] = Time.now
stub_request(:get, described_class.subscription_messages_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) stub_request(:get, described_class.subscription_message_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json)
described_class.update(skip_plugin: true) described_class.update(skip_plugin: true)
notice = described_class.list(include_recently_expired: true).first notice = described_class.list(include_all: true).first
expect(notice.expired?).to eq(true) expect(notice.expired?).to eq(true)
end end
it "dismisses informational subscription notices" do
notice = described_class.list(include_all: true).first
expect(notice.dismissed?).to eq(false)
notice.dismiss!
expect(notice.dismissed?).to eq(true)
end
it "dismisses all informational subscription notices" do
4.times do |index|
subscription_message[:title] += " #{index}"
stub_request(:get, described_class.subscription_message_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json)
described_class.update(skip_plugin: true)
end
expect(described_class.list.count).to eq(5)
described_class.dismiss_all
expect(described_class.list.count).to eq(0)
end
end end
context "plugin status" do context "plugin status" do
before do before do
freeze_time freeze_time
stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: { status: plugin_status }.to_json) stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: plugin_status.to_json)
described_class.update(skip_subscription: true) described_class.update(skip_subscription: true)
end end
it "converts warning into notice" do it "converts warning into notice" do
notice = described_class.list.first notice = described_class.list.first
expect(notice.type).to eq(described_class.types[:plugin_status_warning]) expect(notice.type).to eq(described_class.types[:warning])
expect(notice.message).to eq(I18n.t("wizard.notice.compatibility_issue", domain: described_class.plugin_status_domain)) expect(notice.message).to eq(I18n.t("wizard.notice.compatibility_issue.message", domain: described_class.plugin_status_domain))
expect(notice.created_at.to_datetime).to be_within(1.second).of (plugin_status[:status_changed_at].to_datetime) expect(notice.created_at.to_datetime).to be_within(1.second).of (plugin_status[:status_changed_at].to_datetime)
end end
it "expires warning notices if status is recommended or compatible" do it "expires warning notices if status is recommended or compatible" do
plugin_status[:status] = 'compatible' plugin_status[:status] = 'compatible'
plugin_status[:status_changed_at] = Time.now plugin_status[:status_changed_at] = Time.now
stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: { status: plugin_status }.to_json) stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: plugin_status.to_json)
described_class.update(skip_subscription: true) described_class.update(skip_subscription: true)
notice = described_class.list(type: described_class.types[:plugin_status_warning], include_recently_expired: true).first notice = described_class.list(type: described_class.types[:warning], include_all: true).first
expect(notice.expired?).to eq(true) expect(notice.expired?).to eq(true)
end end
it "hides plugin status warnings" do
notice = described_class.list.first
expect(notice.hidden?).to eq(false)
notice.hide!
expect(notice.hidden?).to eq(true)
end
end end
it "lists notices not expired more than a day ago" do it "lists notices not expired more than a day ago" do
subscription_message[:expired_at] = Time.now - 8.hours subscription_message[:expired_at] = Time.now - 8.hours
stub_request(:get, described_class.subscription_messages_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) stub_request(:get, described_class.subscription_message_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json)
stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: { status: plugin_status }.to_json) stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: plugin_status.to_json)
described_class.update described_class.update
expect(described_class.list(include_recently_expired: true).length).to eq(2) expect(described_class.list(include_all: true).length).to eq(2)
end end
context "connection errors" do context "connection errors" do
@ -84,47 +112,47 @@ describe CustomWizard::Notice do
end end
it "creates an error if connection to notice server fails" do it "creates an error if connection to notice server fails" do
stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: { status: plugin_status }.to_json) stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: plugin_status.to_json)
described_class.update(skip_subscription: true) described_class.update(skip_subscription: true)
error = CustomWizard::Notice::ConnectionError.new(:plugin_status) error = CustomWizard::Notice::ConnectionError.new(:plugin_status)
expect(error.errors.exists?).to eq(true) expect(error.current_error.present?).to eq(true)
end end
it "only creates one connection error per type at a time" do it "only creates one connection error per type at a time" do
stub_request(:get, described_class.subscription_messages_url).to_return(status: 400, body: { messages: [subscription_message] }.to_json) stub_request(:get, described_class.subscription_message_url).to_return(status: 400, body: { messages: [subscription_message] }.to_json)
stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: { status: plugin_status }.to_json) stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: plugin_status.to_json)
5.times { described_class.update } 5.times { described_class.update }
plugin_status_errors = CustomWizard::Notice::ConnectionError.new(:plugin_status) plugin_status_errors = CustomWizard::Notice::ConnectionError.new(:plugin_status)
subscription_message_errors = CustomWizard::Notice::ConnectionError.new(:subscription_messages) subscription_message_errors = CustomWizard::Notice::ConnectionError.new(:subscription_message)
expect(plugin_status_errors.errors.length).to eq(1) expect(plugin_status_errors.current_error[:count]).to eq(5)
expect(subscription_message_errors.errors.length).to eq(1) expect(subscription_message_errors.current_error[:count]).to eq(5)
end end
it "creates a connection error notice if connection errors reach limit" do it "creates a connection error notice if connection errors reach limit" do
stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: { status: plugin_status }.to_json) stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: plugin_status.to_json)
error = CustomWizard::Notice::ConnectionError.new(:plugin_status) error = CustomWizard::Notice::ConnectionError.new(:plugin_status)
error.limit.times { described_class.update(skip_subscription: true) } error.limit.times { described_class.update(skip_subscription: true) }
notice = described_class.list(type: described_class.types[:plugin_status_connection_error]).first notice = described_class.list(type: described_class.types[:connection_error]).first
expect(error.current_error['count']).to eq(error.limit) expect(error.current_error[:count]).to eq(error.limit)
expect(notice.type).to eq(described_class.types[:plugin_status_connection_error]) expect(notice.type).to eq(described_class.types[:connection_error])
end end
it "expires a connection error notice if connection succeeds" do it "expires a connection error notice if connection succeeds" do
stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: { status: plugin_status }.to_json) stub_request(:get, described_class.plugin_status_url).to_return(status: 400, body: plugin_status.to_json)
error = CustomWizard::Notice::ConnectionError.new(:plugin_status) error = CustomWizard::Notice::ConnectionError.new(:plugin_status)
error.limit.times { described_class.update(skip_subscription: true) } error.limit.times { described_class.update(skip_subscription: true) }
stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: { status: plugin_status }.to_json) stub_request(:get, described_class.plugin_status_url).to_return(status: 200, body: plugin_status.to_json)
described_class.update(skip_subscription: true) described_class.update(skip_subscription: true)
notice = described_class.list(type: described_class.types[:plugin_status_connection_error], include_recently_expired: true).first notice = described_class.list(type: described_class.types[:connection_error], include_all: true).first
expect(notice.type).to eq(described_class.types[:plugin_status_connection_error]) expect(notice.type).to eq(described_class.types[:connection_error])
expect(notice.expired_at.present?).to eq(true) expect(notice.expired_at.present?).to eq(true)
end end
end end

Datei anzeigen

@ -186,7 +186,7 @@ describe CustomWizard::Wizard do
it "lists the site categories" do it "lists the site categories" do
Site.clear_cache Site.clear_cache
expect(@wizard.categories.length).to eq(1) expect(@wizard.categories.length > 0).to eq(true)
end end
context "submissions" do context "submissions" do

Datei anzeigen

@ -20,8 +20,8 @@ describe Jobs::CustomWizardUpdateNotices do
} }
it "updates the notices" do it "updates the notices" do
stub_request(:get, CustomWizard::Notice.subscription_messages_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json) stub_request(:get, CustomWizard::Notice.subscription_message_url).to_return(status: 200, body: { messages: [subscription_message] }.to_json)
stub_request(:get, CustomWizard::Notice.plugin_status_url).to_return(status: 200, body: { status: plugin_status }.to_json) stub_request(:get, CustomWizard::Notice.plugin_status_url).to_return(status: 200, body: plugin_status.to_json)
described_class.new.execute described_class.new.execute
expect(CustomWizard::Notice.list.length).to eq(2) expect(CustomWizard::Notice.list.length).to eq(2)

Datei anzeigen

@ -3,29 +3,70 @@ require_relative '../../../plugin_helper'
describe CustomWizard::AdminNoticeController do describe CustomWizard::AdminNoticeController do
fab!(:admin_user) { Fabricate(:user, admin: true) } fab!(:admin_user) { Fabricate(:user, admin: true) }
let(:subscription_message_notice) {
{
title: "Title of message about subscription",
message: "Message about subscription",
type: 0,
created_at: Time.now.iso8601(3),
expired_at: nil
}
}
let(:plugin_status_notice) {
{
title: "The Custom Wizard Plugin is incompatibile with the latest version of Discourse.",
message: "Please check the Custom Wizard Plugin status on [localhost:3000](http://localhost:3000) before updating Discourse.",
type: 1,
archetype: 1,
created_at: Time.now.iso8601(3),
expired_at: nil
}
}
before do before do
sign_in(admin_user) sign_in(admin_user)
@notice = CustomWizard::Notice.new(
message: "Message about subscription",
type: "info",
created_at: Time.now - 3.day,
expired_at: nil
)
@notice.save
end end
it "lists notices" do it "lists notices" do
@notice = CustomWizard::Notice.new(subscription_message_notice)
@notice.save
get "/admin/wizards/notice.json" get "/admin/wizards/notice.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body.length).to eq(1) expect(response.parsed_body.length).to eq(1)
end end
it "dismisses notices" do it "dismisses notices" do
put "/admin/wizards/notice/#{@notice.id}.json" @notice = CustomWizard::Notice.new(subscription_message_notice)
@notice.save
put "/admin/wizards/notice/#{@notice.id}/dismiss.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
updated = CustomWizard::Notice.find(@notice.id) updated = CustomWizard::Notice.find(@notice.id)
expect(updated.dismissed?).to eq(true) expect(updated.dismissed?).to eq(true)
end end
it "dismisses all notices" do
5.times do |index|
subscription_message_notice[:title] += " #{index}"
@notice = CustomWizard::Notice.new(subscription_message_notice)
@notice.save
end
put "/admin/wizards/notice/dismiss.json"
expect(response.status).to eq(200)
expect(CustomWizard::Notice.list.size).to eq(0)
end
it "hides notices" do
@notice = CustomWizard::Notice.new(plugin_status_notice)
@notice.save
put "/admin/wizards/notice/#{@notice.id}/hide.json"
expect(response.status).to eq(200)
updated = CustomWizard::Notice.find(@notice.id)
expect(updated.hidden?).to eq(true)
end
end end