1
0
Fork 0
Dieser Commit ist enthalten in:
angusmcleod 2021-11-01 21:52:29 +08:00
Ursprung bbd1253891
Commit 81bb7e56c2
30 geänderte Dateien mit 930 neuen und 341 gelöschten Zeilen

Datei anzeigen

@ -0,0 +1,14 @@
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,9 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { not, notEmpty } from "@ember/object/computed";
import I18n from "I18n";
import NoticeMessage from "../mixins/notice-message";
export default Component.extend({
classNameBindings: [
":wizard-notice",
"notice.type",
"dismissed",
"expired",
"resolved",
],
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];
},
export default Component.extend(NoticeMessage, {
attributeBindings: ["notice.id:data-notice-id"],
classNameBindings: [':wizard-notice', 'notice.typeClass', 'notice.dismissed:dismissed', 'notice.expired:expired', 'notice.hidden:hidden'],
actions: {
dismiss() {
@ -38,5 +12,12 @@ export default Component.extend({
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,67 @@
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 { popupAjaxError } from "discourse/lib/ajax-error";
import { ajax } from "discourse/lib/ajax";
import Controller, { inject as controller } from "@ember/controller";
import { isPresent } from "@ember/utils";
import { A } from "@ember/array";
export default Controller.extend({
actions: {
dismissNotice(noticeId) {
ajax(`/admin/wizards/notice/${this.id}`, {
type: "DELETE",
})
.then((result) => {
if (result.success) {
const notices = this.notices;
notices.removeObject(notices.findBy("id", noticeId));
}
})
.catch(popupAjaxError);
},
adminWizardsNotices: controller(),
unsubscribe() {
this.messageBus.unsubscribe("/custom-wizard/notices");
},
subscribe() {
this.unsubscribe();
this.messageBus.subscribe("/custom-wizard/notices", (data) => {
if (isPresent(data.active_notice_count)) {
this.set("activeNoticeCount", data.active_notice_count);
this.adminWizardsNotices.setProperties({
notices: A(),
page: 0,
canLoadMore: true
});
this.adminWizardsNotices.loadMoreNotices();
}
});
}
});

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -0,0 +1,65 @@
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,25 +1,68 @@
import EmberObject from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax";
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'),
CustomWizardNotice.reopen({
dismiss() {
return ajax(`/admin/wizards/notice/${this.id}`, { type: "PUT" })
.then((result) => {
if (result.success) {
this.set("dismissed_at", result.dismissed_at);
}
})
.catch(popupAjaxError);
@discourseComputed('type')
typeClass(type) {
return dasherize(type);
},
@discourseComputed('type')
typeLabel(type) {
return I18n.t(`admin.wizard.notice.type.${type}`);
},
dismiss() {
if (!this.get('canDismiss')) {
return;
}
return ajax(`/admin/wizards/notice/${this.get('id')}/dismiss`, { type: 'PUT' }).then(result => {
if (result.success) {
this.set('dismissed_at', result.dismissed_at);
}
}).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({
list() {
return ajax("/admin/wizards/notice").catch(popupAjaxError);
list(data = {}) {
return ajax('/admin/wizards/notice', {
type: "GET",
data
}).catch(popupAjaxError);
},
dismissAll() {
return ajax('/admin/wizards/notice/dismiss', {
type: "PUT"
}).catch(popupAjaxError);
}
});
export default CustomWizardNotice;

Datei anzeigen

@ -0,0 +1,15 @@
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 { ajax } from "discourse/lib/ajax";
import { A } from "@ember/array";
export default DiscourseRoute.extend({
model() {
@ -8,13 +7,21 @@ export default DiscourseRoute.extend({
},
setupController(controller, model) {
controller.set("notices", A(model.notices));
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) {
if (transition.targetName === "adminWizards.index") {
this.transitionTo("adminWizardsWizard");
}
},
}
});

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"}}
<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"}}>
{{d-icon "far-life-ring"}}{{i18n "admin.wizard.support_button.label"}}
</a>
@ -17,8 +23,5 @@
{{/admin-nav}}
<div class="admin-container">
{{#each notices as |notice|}}
{{wizard-notice notice=notice}}
{{/each}}
{{outlet}}
</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">
{{#if resolved}}
<div class="notice-expired-at notice-badge" title={{notice.expired_at}}>
{{d-icon "check"}}
<span class="notice-resolved">{{i18n "admin.wizard.notice.resolved"}}</span>
{{format-date notice.expired_at leaveAgo="true"}}
</div>
<div class="notice-title notice-badge notice-message">
<a role="button" {{action "toggleCookedMessage"}} class="show-notice-message">{{notice.title}}</a>
{{#if showCookedMessage}}
<span class="cooked-notice-message">{{cookedMessage}}</span>
{{/if}}
<div class="notice-title notice-badge" title={{title}}>
{{d-icon icon}}
<span>{{title}}</span>
</div>
<div class="notice-created-at notice-badge" title={{notice.created_at}}>
{{d-icon "far-clock"}}
<span class="notice-issued">{{i18n "admin.wizard.notice.issued"}}</span>
{{format-date notice.created_at leaveAgo="true"}}
</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}}
<div class="notice-updated-at notice-badge" title={{notice.updated_at}}>
{{d-icon "calendar-alt"}}
<span class="notice-updated">{{i18n "admin.wizard.notice.updated"}}</span>
{{format-date notice.updated_at leaveAgo="true"}}
</div>
{{notice-badge class="notice-updated-at" icon="far-clock" label="admin.wizard.notice.updated_at" date=notice.updated_at}}
{{/if}}
<div class="notice-plugin notice-badge" title={{i18n "admin.wizard.notice.plugin"}}>
{{d-icon "plug"}}
<span>{{i18n "admin.wizard.notice.plugin"}}</span>
</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 notice.canDismiss}}
<div class="dismiss-notice-container">
{{#if dismissing}}
{{loading-spinner size="small"}}
{{else}}
<a {{action "dismiss"}} role="button" class="dismiss-notice">{{d-icon "times"}}</a>
{{/if}}
</div>
{{/if}}
{{#if notice.canHide}}
<div class="hide-notice-container">
{{#if hiding}}
{{loading-spinner size="small"}}
{{else}}
<a {{action "hide"}} role="button" class="hide-notice">{{d-icon "far-eye-slash"}}</a>
{{/if}}
</div>
{{/if}}
</div>
</div>

Datei anzeigen

@ -4,15 +4,25 @@
@import "common/components/buttons";
@import "wizard/variables";
$expired: #339b18;
$info: #038ae7;
$warning: #d47e00;
$error: #ef1700;
.admin-wizard-controls {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
min-height: 34px;
& + .wizard-message + div {
margin-top: 20px;
}
h3 {
margin-bottom: 0;
}
}
.wizard-message {
@ -909,87 +919,142 @@
}
}
.admin-wizards-notices {
.wizard-table {
overflow: unset;
}
}
.wizard-notice {
padding: 1em;
padding: .75em;
margin-bottom: 1em;
border: 1px solid var(--primary);
position: relative;
border: 1px solid var(--primary-low);
&.dismissed {
display: none;
}
&.resolved .notice-badge:not(.notice-expired-at),
&.resolved a,
&.resolved p {
&.expired .notice-badge:not(.notice-expired-at),
&.expired a,
&.expired p {
color: var(--primary-medium) !important;
}
.d-icon {
margin-right: 0.4em;
.notice-badge {
padding: 0 .5em;
}
.notice-header {
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 {
border: 1px solid var(--pavilion-warning);
background-color: rgba($pavilion_warning, 0.1);
color: var(--pavilion-warning);
padding: 0;
}
.notice-header-right {
margin-left: auto;
display: flex;
align-items: center;
.notice-badge {
margin-left: .5em;
}
}
}
.notice-issued,
.notice-resolved {
margin-right: 0.3em;
}
.notice-message {
p {
margin: 0.5em 0;
}
p:last-of-type {
margin-bottom: 0;
}
.dismiss-notice-container,
.hide-notice-container {
width: 40px;
display: flex;
justify-content: center;
align-items: center;
}
.dismiss-notice,
.spinner {
position: absolute;
top: 1em;
right: 1em;
color: var(--primary-medium);
.hide-notice {
display: flex;
align-items: center;
.d-icon {
margin-right: 0;
color: var(--primary);
}
}
}
.disable-important {
.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;
right: 3em;
top: 1em;
color: var(--primary-medium);
top: -8px;
right: -8px;
font-size: .7em;
}
a.show-notice-message {
padding: .25em .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;
justify-content: flex-start;
h3 {
margin-bottom: 0;
}
.buttons {
display: flex;
margin-left: auto;

Datei anzeigen

@ -112,6 +112,9 @@ en:
select: "Select a wizard to see its logs"
viewing: "View recent logs for wizards on the forum"
documentation: "Check out the logs documentation"
notices:
info: "Plugin status and subscription notices"
documentation: Check out the notices documentation
editor:
show: "Show"
@ -485,14 +488,31 @@ en:
notice:
plugin: Custom Wizard Plugin
issued: Issued
update: Updated
resolved: Resolved
title:
plugin_status_warning: Warning Notice
plugin_status_connection_error: Connection Notice
subscription_messages_connection_error: Connection Notice
disable_important_on_dashboard: disable
message: Message
time: Time
status: Status
title: Title
dismiss:
label: Dismiss
title: Dismiss notice
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:
group:

Datei anzeigen

@ -54,21 +54,21 @@ en:
notice:
connection_error: "Failed to connect to http://%{domain}"
compatibility_issue: >
The Custom Wizard Plugin has a compatibility issue with the latest version of Discourse.
Please check the Custom Wizard Plugin status on [%{domain}](http://%{domain}) before updating Discourse.
compatibility_issue:
title: The Custom Wizard Plugin is incompatibile with the latest version of Discourse.
message: Please check the Custom Wizard Plugin status on [%{domain}](http://%{domain}) before updating Discourse.
plugin_status:
connection_error_limit: >
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.
If this connection issue persists please contact <a href="mailto:support@thepavilion.io">support@thepavilion.io</a> for further assistance.
subscription_messages:
connection_error_limit: >
We're unable to connect to the Pavilion Subscription Server. This will not affect the operation of the plugin.
If this connection issue persists please contact <a href="mailto:support@thepavilion.io">support@thepavilion.io</a> for further assistance.
connection_error:
title: Unable to connect to the Custom Wizard Plugin status server
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_message:
connection_error:
title: Unable to connect to the Custom Wizard Plugin subscription server
message: If this issue persists contact <a href="mailto:support@thepavilion.io">support@thepavilion.io</a> for further assistance.
site_settings:
custom_wizard_enabled: "Enable custom wizards."
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_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'
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

Datei anzeigen

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

Datei anzeigen

@ -1,20 +1,66 @@
# frozen_string_literal: true
class CustomWizard::AdminNoticeController < CustomWizard::AdminController
before_action :find_notice, only: [:dismiss]
before_action :find_notice, only: [:dismiss, :hide]
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 { |type| CustomWizard::Notice.types[type.to_sym] }
else
type = CustomWizard::Notice.types[type.to_sym]
end
end
if archetype
if archetype.is_a?(Array)
archetype = archetype.map { |type| 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
def dismiss
if @notice.dismissable? && @notice.dismiss
if @notice.dismissable? && @notice.dismiss!
render json: success_json.merge(dismissed_at: @notice.dismissed_at)
else
render json: failed_json
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
@notice = CustomWizard::Notice.find(params[:notice_id])
raise Discourse::InvalidParameters.new(:notice_id) unless @notice

Datei anzeigen

@ -7,42 +7,61 @@ class CustomWizard::Notice
"tests-passed" => "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"
PLUGIN_STATUSES_TO_WARN = %w(incompatible tests_failing)
CHECK_PLUGIN_STATUS_ON_BRANCH = %w(tests-passed main stable)
PAGE_LIMIT = 30
attr_reader :id,
:title,
:message,
:type,
:archetype,
:created_at
attr_accessor :retrieved_at,
:updated_at,
:dismissed_at,
:expired_at
:expired_at,
:hidden_at
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]
@type = attrs[:type].to_i
@archetype = attrs[:archetype].to_i
@created_at = attrs[:created_at]
@updated_at = attrs[:updated_at]
@retrieved_at = attrs[:retrieved_at]
@dismissed_at = attrs[:dismissed_at]
@expired_at = attrs[:expired_at]
@hidden_at = attrs[:hidden_at]
end
def dismiss
def dismiss!
if dismissable?
self.dismissed_at = Time.now
self.save
self.class.publish_notice_count
end
end
def expire
def hide!
if can_hide?
self.hidden_at = Time.now
self.save
self.class.publish_notice_count
end
end
def expire!
if !expired?
self.expired_at = Time.now
self.save
self.class.publish_notice_count
end
end
def expired?
@ -54,15 +73,33 @@ class CustomWizard::Notice
end
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
def save
attrs = {
expired_at: expired_at,
updated_at: updated_at,
retrieved_at: retrieved_at,
created_at: created_at,
hidden_at: hidden_at,
title: title,
message: message,
type: type
type: type,
archetype: archetype
}
if current = self.class.find(self.id)
@ -75,9 +112,15 @@ class CustomWizard::Notice
def self.types
@types ||= Enum.new(
info: 0,
plugin_status_warning: 1,
plugin_status_connection_error: 2,
subscription_messages_connection_error: 3
warning: 1,
connection_error: 2
)
end
def self.archetypes
@archetypes ||= Enum.new(
subscription_message: 0,
plugin_status: 1
)
end
@ -85,7 +128,7 @@ class CustomWizard::Notice
notices = []
if !skip_subscription
subscription_messages = request(:subscription_messages)
subscription_messages = request(:subscription_message)
if subscription_messages.present?
subscription_notices = convert_subscription_messages_to_notices(subscription_messages[:messages])
@ -96,27 +139,46 @@ class CustomWizard::Notice
if !skip_plugin && request_plugin_status?
plugin_status = request(:plugin_status)
if plugin_status.present? && plugin_status[:status].present? && plugin_status[:status].is_a?(Hash)
plugin_notice = convert_plugin_status_to_notice(plugin_status[:status])
if plugin_status.present? && plugin_status[:status].present?
plugin_notice = convert_plugin_status_to_notice(plugin_status)
notices.push(plugin_notice) if plugin_notice
end
end
if notices.any?
notices.each do |notice_data|
notice = new(notice_data)
notice.retrieved_at = Time.now
notice.save
end
publish_notice_count
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)
messages.map do |message|
{
messages.reduce([]) do |result, message|
notice_id = generate_notice_id(message[:title], message[:created_at])
unless exists?(notice_id)
result.push(
title: message[:title],
message: message[:message],
type: types[message[:type].to_sym],
archetype: archetypes[:subscription_message],
created_at: message[:created_at],
expired_at: message[:expired_at]
}
)
end
result
end
end
@ -124,22 +186,32 @@ class CustomWizard::Notice
notice = nil
if PLUGIN_STATUSES_TO_WARN.include?(plugin_status[:status])
title = I18n.t('wizard.notice.compatibility_issue.title')
created_at = plugin_status[:status_changed_at]
id = generate_notice_id(title, created_at)
unless exists?(id)
message = I18n.t('wizard.notice.compatibility_issue.message', domain: plugin_status_domain)
notice = {
message: I18n.t('wizard.notice.compatibility_issue', domain: plugin_status_domain),
type: types[:plugin_status_warning],
created_at: plugin_status[:status_changed_at]
id: id,
title: title,
message: message,
type: types[:warning],
archetype: archetypes[:plugin_status],
created_at: created_at
}
end
else
expire_notices(types[:plugin_status_warning])
expire_all(types[:warning], archetypes[:plugin_status])
end
notice
end
def self.notify_connection_errors(connection_type_key)
domain = self.send("#{connection_type_key.to_s}_domain")
message = I18n.t("wizard.notice.#{connection_type_key.to_s}.connection_error_limit", domain: domain)
notices = list(type: types[:connection_error], message: message)
def self.notify_connection_errors(archetype)
domain = self.send("#{archetype.to_s}_domain")
title = I18n.t("wizard.notice.#{archetype.to_s}.connection_error.title")
notices = list(type: types[:connection_error], archetype: archetypes[archetype.to_sym], title: title)
if notices.any?
notice = notices.first
@ -147,28 +219,29 @@ class CustomWizard::Notice
notice.save
else
notice = new(
message: message,
type: types["#{connection_type_key}_connection_error".to_sym],
created_at: Time.now
title: title,
message: I18n.t("wizard.notice.#{archetype.to_s}.connection_error.message", domain: domain),
archetype: archetypes[archetype.to_sym],
type: types[:connection_error],
created_at: Time.now,
updated_at: Time.now
)
notice.save
end
end
def self.expire_notices(type)
list(type: type).each(&:expire)
publish_notice_count
end
def self.request_plugin_status?
CHECK_PLUGIN_STATUS_ON_BRANCH.include?(Discourse.git_branch) || Rails.env.test? || Rails.env.development?
end
def self.subscription_messages_domain
(Rails.env.test? || Rails.env.development?) ? LOCALHOST_DOMAIN : SUBSCRIPTION_MESSAGES_DOMAIN
def self.subscription_message_domain
(Rails.env.test? || Rails.env.development?) ? LOCALHOST_DOMAIN : SUBSCRIPTION_MESSAGE_DOMAIN
end
def self.subscription_messages_url
"http://#{subscription_messages_domain}/subscription-server/messages.json"
def self.subscription_message_url
"http://#{subscription_message_domain}/subscription-server/messages.json"
end
def self.plugin_status_domain
@ -180,14 +253,19 @@ class CustomWizard::Notice
"http://#{plugin_status_domain}/plugin-manager/status/discourse-custom-wizard"
end
def self.request(type)
url = self.send("#{type.to_s}_url")
response = Excon.get(url)
connection_error = CustomWizard::Notice::ConnectionError.new(type)
def self.request(archetype)
url = self.send("#{archetype.to_s}_url")
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!
expire_notices(types["#{type}_connection_error".to_sym])
expire_all(types[:connection_error], archetypes[archetype.to_sym])
begin
data = JSON.parse(response.body).deep_symbolize_keys
@ -198,8 +276,7 @@ class CustomWizard::Notice
data
else
connection_error.create!
notify_connection_errors(type) if connection_error.reached_limit?
notify_connection_errors(archetype) if connection_error.reached_limit?
nil
end
end
@ -213,20 +290,65 @@ class CustomWizard::Notice
new(raw.symbolize_keys) if raw.present?
end
def self.exists?(id)
PluginStoreRow.where(plugin_name: namespace, key: id).exists?
end
def self.store(id, raw_notice)
PluginStore.set(namespace, id, raw_notice)
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, include_recently_expired: false, page: nil, visible: false)
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->>'type')::integer = ?", type) if type
query = query.where("(value::json->>'message')::text = ?", message) if message
query.order("value::json->>'created_at' DESC")
query = query.where("(value::json->>'hidden_at') IS NULL") if visible
query = query.where("(value::json->>'dismissed_at') IS NULL") unless include_all
query = query.where("(value::json->>'expired_at') IS NULL#{include_recently_expired ? " OR (value::json->>'expired_at')::date > now()::date - 1" : ""}") unless include_all
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
def self.list(type: nil, message: nil, include_recently_expired: false)
list_query(type: type, message: message, include_recently_expired: include_recently_expired)
def self.list(type: nil, archetype: nil, title: nil, include_all: false, include_recently_expired: false, page: 0, visible: false)
list_query(type: type, archetype: archetype, title: title, include_all: include_all, include_recently_expired: include_recently_expired, page: page, visible: visible)
.map { |r| self.new(JSON.parse(r.value).symbolize_keys) }
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

Datei anzeigen

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

Datei anzeigen

@ -245,7 +245,10 @@ after_initialize do
end
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
end

Datei anzeigen

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