DEV: Modernize the remaining admin-webhooks parts (#19438)

This commit is contained in:
Jarek Radosz 2022-12-13 13:32:34 +01:00 committed by GitHub
parent f19d687f01
commit fd405179a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 260 additions and 246 deletions

View File

@ -1,47 +0,0 @@
import Component from "@ember/component";
import I18n from "I18n";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["hook-event"],
typeName: alias("type.name"),
@discourseComputed("typeName")
name(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.name`);
},
@discourseComputed("typeName")
details(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.details`);
},
@discourseComputed("model.[]", "typeName")
eventTypeExists(eventTypes, typeName) {
return eventTypes.any((event) => event.name === typeName);
},
@discourseComputed("eventTypeExists")
enabled: {
get(eventTypeExists) {
return eventTypeExists;
},
set(value, eventTypeExists) {
const type = this.type;
const model = this.model;
// add an association when not exists
if (value !== eventTypeExists) {
if (value) {
model.addObject(type);
} else {
model.removeObjects(
model.filter((eventType) => eventType.name === type.name)
);
}
}
return value;
},
},
});

View File

@ -1,110 +0,0 @@
import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter";
import Component from "@ember/component";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
export default Component.extend({
tagName: "li",
expandDetails: null,
expandDetailsRequestKey: "request",
expandDetailsResponseKey: "response",
dialog: service(),
@discourseComputed("model.status")
statusColorClasses(status) {
if (!status) {
return "";
}
if (status >= 200 && status <= 299) {
return "text-successful";
} else {
return "text-danger";
}
},
@discourseComputed("model.created_at")
createdAt(createdAt) {
return moment(createdAt).format("YYYY-MM-DD HH:mm:ss");
},
@discourseComputed("model.duration")
completion(duration) {
const seconds = Math.floor(duration / 10.0) / 100.0;
return I18n.t("admin.web_hooks.events.completed_in", { count: seconds });
},
@discourseComputed("expandDetails")
expandRequestIcon(expandDetails) {
return expandDetails === this.expandDetailsRequestKey
? "ellipsis-h"
: "ellipsis-v";
},
@discourseComputed("expandDetails")
expandResponseIcon(expandDetails) {
return expandDetails === this.expandDetailsResponseKey
? "ellipsis-h"
: "ellipsis-v";
},
actions: {
redeliver() {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.web_hooks.events.redeliver_confirm"),
didConfirm: () => {
return ajax(
`/admin/api/web_hooks/${this.get(
"model.web_hook_id"
)}/events/${this.get("model.id")}/redeliver`,
{ type: "POST" }
)
.then((json) => {
this.set("model", json.web_hook_event);
})
.catch(popupAjaxError);
},
});
},
toggleRequest() {
const expandDetailsKey = this.expandDetailsRequestKey;
if (this.expandDetails !== expandDetailsKey) {
let headers = Object.assign(
{
"Request URL": this.get("model.request_url"),
"Request method": "POST",
},
ensureJSON(this.get("model.headers"))
);
this.setProperties({
headers: plainJSON(headers),
body: prettyJSON(this.get("model.payload")),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.payload"),
});
} else {
this.set("expandDetails", null);
}
},
toggleResponse() {
const expandDetailsKey = this.expandDetailsResponseKey;
if (this.expandDetails !== expandDetailsKey) {
this.setProperties({
headers: plainJSON(this.get("model.response_headers")),
body: this.get("model.response_body"),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.body"),
});
} else {
this.set("expandDetails", null);
}
},
},
});

View File

@ -1,39 +0,0 @@
import Component from "@ember/component";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { iconHTML } from "discourse-common/lib/icon-library";
import { htmlSafe } from "@ember/template";
export default Component.extend({
classes: ["text-muted", "text-danger", "text-successful", "text-muted"],
icons: ["far-circle", "times-circle", "circle", "circle"],
circleIcon: null,
deliveryStatus: null,
@discourseComputed("deliveryStatuses", "model.last_delivery_status")
status(deliveryStatuses, lastDeliveryStatus) {
return deliveryStatuses.find((s) => s.id === lastDeliveryStatus);
},
@discourseComputed("status.id", "icons")
icon(statusId, icons) {
return icons[statusId - 1];
},
@discourseComputed("status.id", "classes")
class(statusId, classes) {
return classes[statusId - 1];
},
didReceiveAttrs() {
this._super(...arguments);
this.set(
"circleIcon",
htmlSafe(iconHTML(this.icon, { class: this.class }))
);
this.set(
"deliveryStatus",
I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`)
);
},
});

View File

@ -1,4 +1,4 @@
<label>
<label class="hook-event">
<Input
@type="checkbox"
@checked={{this.enabled}}

View File

@ -0,0 +1,41 @@
import Component from "@glimmer/component";
import I18n from "I18n";
export default class WebhookEventChooser extends Component {
get name() {
return I18n.t(`admin.web_hooks.${this.args.type.name}_event.name`);
}
get details() {
return I18n.t(`admin.web_hooks.${this.args.type.name}_event.details`);
}
get eventTypeExists() {
return this.args.eventTypes.any(
(event) => event.name === this.args.type.name
);
}
get enabled() {
return this.eventTypeExists;
}
set enabled(value) {
const eventTypes = this.args.eventTypes;
// add an association when not exists
if (value === this.eventTypeExists) {
return value;
}
if (value) {
eventTypes.addObject(this.args.type);
} else {
eventTypes.removeObjects(
eventTypes.filter((eventType) => eventType.name === this.args.type.name)
);
}
return value;
}
}

View File

@ -0,0 +1,39 @@
<li>
<div class="col first status">
<span class={{this.statusColorClasses}}>{{@event.status}}</span>
</div>
<div class="col event-id">{{@event.id}}</div>
<div class="col timestamp">{{this.createdAt}}</div>
<div class="col completion">{{this.completion}}</div>
<div class="col actions">
<DButton
@icon={{this.expandRequestIcon}}
@action={{this.toggleRequest}}
@label="admin.web_hooks.events.request"
/>
<DButton
@icon={{this.expandResponseIcon}}
@action={{this.toggleResponse}}
@label="admin.web_hooks.events.response"
/>
<DButton
@icon="sync"
@action={{this.redeliver}}
@label="admin.web_hooks.events.redeliver"
/>
</div>
{{#if this.expandDetails}}
<div class="details">
<h3>{{i18n "admin.web_hooks.events.headers"}}</h3>
<pre><code>{{this.headers}}</code></pre>
<h3>{{this.bodyLabel}}</h3>
<pre><code>{{this.body}}</code></pre>
</div>
{{/if}}
</li>

View File

@ -0,0 +1,102 @@
import Component from "@glimmer/component";
import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter";
import I18n from "I18n";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
export default class WebhookEvent extends Component {
@service dialog;
@tracked body = "";
@tracked bodyLabel = "";
@tracked expandDetails = null;
@tracked headers = "";
expandDetailsRequestKey = "request";
expandDetailsResponseKey = "response";
get statusColorClasses() {
const { status } = this.args.event;
if (!status) {
return "";
}
if (status >= 200 && status <= 299) {
return "text-successful";
} else {
return "text-danger";
}
}
get createdAt() {
return moment(this.args.event.created_at).format("YYYY-MM-DD HH:mm:ss");
}
get completion() {
const seconds = Math.floor(this.args.event.duration / 10.0) / 100.0;
return I18n.t("admin.web_hooks.events.completed_in", { count: seconds });
}
get expandRequestIcon() {
return this.expandDetails === this.expandDetailsRequestKey
? "ellipsis-h"
: "ellipsis-v";
}
get expandResponseIcon() {
return this.expandDetails === this.expandDetailsResponseKey
? "ellipsis-h"
: "ellipsis-v";
}
@action
redeliver() {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.web_hooks.events.redeliver_confirm"),
didConfirm: async () => {
try {
const json = await ajax(
`/admin/api/web_hooks/${this.args.event.web_hook_id}/events/${this.args.event.id}/redeliver`,
{ type: "POST" }
);
this.args.event.setProperties(json.web_hook_event);
} catch (e) {
popupAjaxError(e);
}
},
});
}
@action
toggleRequest() {
if (this.expandDetails !== this.expandDetailsRequestKey) {
const headers = {
"Request URL": this.args.event.request_url,
"Request method": "POST",
...ensureJSON(this.args.event.headers),
};
this.headers = plainJSON(headers);
this.body = prettyJSON(this.args.event.payload);
this.expandDetails = this.expandDetailsRequestKey;
this.bodyLabel = I18n.t("admin.web_hooks.events.payload");
} else {
this.expandDetails = null;
}
}
@action
toggleResponse() {
if (this.expandDetails !== this.expandDetailsResponseKey) {
this.headers = plainJSON(this.args.event.response_headers);
this.body = this.args.event.response_body;
this.expandDetails = this.expandDetailsResponseKey;
this.bodyLabel = I18n.t("admin.web_hooks.events.body");
} else {
this.expandDetails = null;
}
}
}

View File

@ -28,8 +28,8 @@
{{/if}}
<ul>
{{#each this.events as |webHookEvent|}}
<AdminWebHookEvent @model={{webHookEvent}} />
{{#each this.events as |event|}}
<WebhookEvent @event={{event}} />
{{/each}}
</ul>
</div>

View File

@ -61,8 +61,8 @@ export default class WebhookEvents extends Component {
data: { ids: this.incomingEventIds },
});
const objects = data.map((webHookEvent) =>
this.store.createRecord("web-hook-event", webHookEvent)
const objects = data.map((webhookEvent) =>
this.store.createRecord("web-hook-event", webhookEvent)
);
this.events.unshiftObjects(objects);
this.incomingEventIds = [];

View File

@ -0,0 +1,2 @@
{{d-icon this.iconName (hash class=this.iconClass)}}
{{this.deliveryStatus}}

View File

@ -0,0 +1,24 @@
import Component from "@glimmer/component";
import I18n from "I18n";
export default class WebhookStatus extends Component {
iconNames = ["far-circle", "times-circle", "circle", "circle"];
iconClasses = ["text-muted", "text-danger", "text-successful", "text-muted"];
get status() {
const lastStatus = this.args.webhook.last_delivery_status;
return this.args.deliveryStatuses.find((s) => s.id === lastStatus);
}
get deliveryStatus() {
return I18n.t(`admin.web_hooks.delivery_status.${this.status.name}`);
}
get iconName() {
return this.iconNames[this.status.id - 1];
}
get iconClass() {
return this.iconClasses[this.status.id - 1];
}
}

View File

@ -15,7 +15,7 @@ export default RestModel.extend({
groupsFilterInName: null,
@discourseComputed("wildcard_web_hook")
webHookType: {
webhookType: {
get(wildcard) {
return wildcard ? "wildcard" : "individual";
},

View File

@ -1,19 +0,0 @@
<div class="col first status">
<span class={{this.statusColorClasses}}>{{this.model.status}}</span>
</div>
<div class="col event-id">{{this.model.id}}</div>
<div class="col timestamp">{{this.createdAt}}</div>
<div class="col completion">{{this.completion}}</div>
<div class="col actions">
<DButton @icon={{this.expandRequestIcon}} @action={{action "toggleRequest"}} @label="admin.web_hooks.events.request" />
<DButton @icon={{this.expandResponseIcon}} @action={{action "toggleResponse"}} @label="admin.web_hooks.events.response" />
<DButton @icon="sync" @action={{action "redeliver"}} @label="admin.web_hooks.events.redeliver" />
</div>
{{#if this.expandDetails}}
<div class="details">
<h3>{{i18n "admin.web_hooks.events.headers"}}</h3>
<pre><code>{{this.headers}}</code></pre>
<h3>{{this.bodyLabel}}</h3>
<pre><code>{{this.body}}</code></pre>
</div>
{{/if}}

View File

@ -1 +0,0 @@
{{this.circleIcon}} {{this.deliveryStatus}}

View File

@ -26,8 +26,13 @@
<div class="control-group">
<label>{{i18n "admin.web_hooks.event_chooser"}}</label>
<label>
<RadioButton @class="subscription-choice" @name="subscription-choice" @value="individual" @selection={{this.model.webHookType}} />
<label class="subscription-choice">
<RadioButton
@name="subscription-choice"
@value="individual"
@selection={{this.model.webhookType}}
/>
{{i18n "admin.web_hooks.individual_event"}}
<InputTip @validation={{this.eventTypeValidation}} />
</label>
@ -35,13 +40,20 @@
{{#unless this.model.wildcard_web_hook}}
<div class="event-selector">
{{#each this.eventTypes as |type|}}
<AdminWebHookEventChooser @type={{type}} @model={{this.model.web_hook_event_types}} />
<WebhookEventChooser
@type={{type}}
@eventTypes={{this.model.web_hook_event_types}}
/>
{{/each}}
</div>
{{/unless}}
<label>
<RadioButton @class="subscription-choice" @name="subscription-choice" @value="wildcard" @selection={{this.model.webHookType}} />
<label class="subscription-choice">
<RadioButton
@name="subscription-choice"
@value="wildcard"
@selection={{this.model.webhookType}}
/>
{{i18n "admin.web_hooks.wildcard_event"}}
</label>
</div>

View File

@ -24,24 +24,26 @@
</tr>
</thead>
<tbody>
{{#each this.model as |webHook|}}
{{#each this.model as |webhook|}}
<tr>
<td class="delivery-status">
<LinkTo @route="adminWebHooks.show" @model={{webHook}}>
<AdminWebHookStatus
<LinkTo @route="adminWebHooks.show" @model={{webhook}}>
<WebhookStatus
@deliveryStatuses={{this.deliveryStatuses}}
@model={{webHook}}
@webhook={{webhook}}
/>
</LinkTo>
</td>
<td class="payload-url">
<LinkTo @route="adminWebHooks.edit" @model={{webHook}}>{{webHook.payload_url}}</LinkTo>
<LinkTo @route="adminWebHooks.edit" @model={{webhook}}>
{{webhook.payload_url}}
</LinkTo>
</td>
<td class="description">{{webHook.description}}</td>
<td class="description">{{webhook.description}}</td>
<td class="controls">
<LinkTo
@route="adminWebHooks.edit"
@model={{webHook}}
@model={{webhook}}
class="btn btn-default no-text"
title={{i18n "admin.web_hooks.edit"}}
>
@ -51,7 +53,7 @@
<DButton
@class="destroy btn-danger"
@action={{this.destroy}}
@actionParam={{webHook}}
@actionParam={{webhook}}
@icon="times"
@title="delete"
/>

View File

@ -65,16 +65,19 @@ export function enableMissingIconWarning() {
}
export function renderIcon(renderType, id, params) {
for (let i = 0; i < _renderers.length; i++) {
let renderer = _renderers[i];
let rendererForType = renderer[renderType];
params ||= {};
if (rendererForType) {
const icon = { id, replacementId: REPLACEMENTS[id] };
let result = rendererForType(icon, params || {});
if (result) {
return result;
}
for (const renderer of _renderers) {
const rendererForType = renderer[renderType];
if (!rendererForType) {
continue;
}
const icon = { id, replacementId: REPLACEMENTS[id] };
const result = rendererForType(icon, params);
if (result) {
return result;
}
}
}

View File

@ -79,7 +79,7 @@ export default Component.extend({
: this._reset("invisible");
},
_set(name, icon, key, iconArgs = null) {
_set(name, icon, key, iconArgs) {
this.set(`${name}Icon`, htmlSafe(iconHTML(`${icon}`, iconArgs)));
this.set(`${name}Title`, I18n.t(`topic_statuses.${key}.help`));
return true;

View File

@ -15,7 +15,7 @@ export default createWidget("topic-status", {
const result = [];
TopicStatusIcons.render(topic, function (name, key) {
const iconArgs = key === "unpinned" ? { class: "unpinned" } : null;
const iconArgs = { class: key === "unpinned" ? "unpinned" : null };
const icon = iconNode(name, iconArgs);
const attributes = {

View File

@ -260,8 +260,13 @@ table.api-keys {
.instructions {
margin-top: 5px;
}
.subscription-choice {
margin-bottom: 10px;
label {
display: inline-block;
}
}
}