mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
Admin webhooks UI guidelines (#30764)
This change converts the admin webhooks UI to the new UI guidelines and modernizes the JS.
This commit is contained in:
parent
35507d4090
commit
5c0b7c4d70
@ -0,0 +1,301 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { concat, hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { eq } from "truth-helpers";
|
||||
import BackButton from "discourse/components/back-button";
|
||||
import ConditionalLoadingSection from "discourse/components/conditional-loading-section";
|
||||
import Form from "discourse/components/form";
|
||||
import GroupSelector from "discourse/components/group-selector";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import WebhookEventChooser from "admin/components/webhook-event-chooser";
|
||||
import CategorySelector from "select-kit/components/category-selector";
|
||||
import TagChooser from "select-kit/components/tag-chooser";
|
||||
|
||||
export default class AdminConfigAreasWebhookForm extends Component {
|
||||
@service router;
|
||||
@service siteSettings;
|
||||
@service store;
|
||||
|
||||
@tracked webhook = this.args.webhook;
|
||||
|
||||
@tracked loadingExtras = true;
|
||||
|
||||
@tracked webhookEventTypes = [];
|
||||
@tracked defaultEventTypes = {};
|
||||
@tracked groupedEventTypes = {};
|
||||
@tracked contentTypes = [];
|
||||
@tracked deliveryStatuses = [];
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
this.#loadExtras();
|
||||
}
|
||||
|
||||
@cached
|
||||
get formData() {
|
||||
return {
|
||||
payload_url: this.webhook.payload_url,
|
||||
content_type: this.webhook.content_type,
|
||||
secret: this.webhook.secret === "" ? null : this.webhook.secret,
|
||||
categories: this.webhook.categories,
|
||||
group_names: this.webhook.group_names,
|
||||
tag_names: this.webhook.tag_names,
|
||||
wildcard: this.webhook.wildcard,
|
||||
web_hook_event_types: this.webhook.web_hook_event_types,
|
||||
verify_certificate: this.webhook.verify_certificate,
|
||||
active: this.webhook.active,
|
||||
};
|
||||
}
|
||||
|
||||
async #loadExtras() {
|
||||
try {
|
||||
this.loadingExtras = true;
|
||||
|
||||
const webhooks = await this.store.findAll("web-hook");
|
||||
|
||||
this.groupedEventTypes = webhooks.extras.grouped_event_types;
|
||||
this.defaultEventTypes = webhooks.extras.default_event_types;
|
||||
this.contentTypes = webhooks.extras.content_types;
|
||||
this.deliveryStatuses = webhooks.extras.delivery_statuses;
|
||||
|
||||
if (this.webhook.isNew) {
|
||||
this.webhookEventTypes = [...this.defaultEventTypes];
|
||||
} else {
|
||||
this.webhookEventTypes = [...this.webhook.web_hook_event_types];
|
||||
}
|
||||
} finally {
|
||||
this.loadingExtras = false;
|
||||
}
|
||||
}
|
||||
|
||||
get showTagsFilter() {
|
||||
return this.siteSettings.tagging_enabled;
|
||||
}
|
||||
|
||||
get saveButtonLabel() {
|
||||
return this.webhook.isNew
|
||||
? "admin.web_hooks.create"
|
||||
: "admin.web_hooks.save";
|
||||
}
|
||||
|
||||
@action
|
||||
async save(data) {
|
||||
try {
|
||||
const isNew = this.webhook.isNew;
|
||||
|
||||
this.webhook.setProperties({
|
||||
...data,
|
||||
web_hook_event_types: this.webhookEventTypes,
|
||||
});
|
||||
|
||||
await this.webhook.save();
|
||||
|
||||
if (isNew) {
|
||||
this.router.transitionTo("adminWebHooks.show", this.webhook);
|
||||
} else {
|
||||
this.router.transitionTo("adminWebHooks.index");
|
||||
}
|
||||
} catch (e) {
|
||||
popupAjaxError(e);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<BackButton @route="adminWebHooks.index" @label="admin.web_hooks.back" />
|
||||
|
||||
<div class="admin-config-area user-field">
|
||||
<div class="admin-config-area__primary-content">
|
||||
<div class="admin-config-area-card">
|
||||
<div class="web-hook-container">
|
||||
<ConditionalLoadingSection @isLoading={{this.loadingExtras}}>
|
||||
<p>{{i18n "admin.web_hooks.detailed_instruction"}}</p>
|
||||
<Form
|
||||
@onSubmit={{this.save}}
|
||||
@data={{this.formData}}
|
||||
as |form transientData|
|
||||
>
|
||||
<form.Field
|
||||
@name="payload_url"
|
||||
@title={{i18n "admin.web_hooks.payload_url"}}
|
||||
@format="large"
|
||||
@validation="required|url"
|
||||
as |field|
|
||||
>
|
||||
<field.Input
|
||||
placeholder={{i18n
|
||||
"admin.web_hooks.payload_url_placeholder"
|
||||
}}
|
||||
/>
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="content_type"
|
||||
@title={{i18n "admin.web_hooks.content_type"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Select as |select|>
|
||||
{{#each this.contentTypes as |contentType|}}
|
||||
<select.Option
|
||||
@value={{contentType.id}}
|
||||
>{{contentType.name}}</select.Option>
|
||||
{{/each}}
|
||||
</field.Select>
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="secret"
|
||||
@title={{i18n "admin.web_hooks.secret"}}
|
||||
@description={{i18n "admin.web_hooks.secret_placeholder"}}
|
||||
@format="large"
|
||||
@validation="length:12"
|
||||
as |field|
|
||||
>
|
||||
<field.Input />
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="wildcard"
|
||||
@title={{i18n "admin.web_hooks.event_chooser"}}
|
||||
@validation="required"
|
||||
@onSet={{this.setRequirement}}
|
||||
@format="full"
|
||||
as |field|
|
||||
>
|
||||
<field.RadioGroup as |radioGroup|>
|
||||
<radioGroup.Radio @value="individual">
|
||||
{{i18n "admin.web_hooks.individual_event"}}
|
||||
</radioGroup.Radio>
|
||||
{{#if (eq transientData.wildcard "individual")}}
|
||||
<div class="event-selector">
|
||||
{{#each-in
|
||||
this.groupedEventTypes
|
||||
as |group eventTypes|
|
||||
}}
|
||||
<div class="event-group">
|
||||
{{i18n
|
||||
(concat
|
||||
"admin.web_hooks." group "_event.group_name"
|
||||
)
|
||||
}}
|
||||
{{#each eventTypes as |type|}}
|
||||
<WebhookEventChooser
|
||||
@type={{type}}
|
||||
@group={{group}}
|
||||
@eventTypes={{this.webhookEventTypes}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each-in}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<radioGroup.Radio @value="wildcard">
|
||||
{{i18n "admin.web_hooks.wildcard_event"}}
|
||||
</radioGroup.Radio>
|
||||
</field.RadioGroup>
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="categories"
|
||||
@title={{i18n "admin.web_hooks.categories_filter"}}
|
||||
@description={{i18n
|
||||
"admin.web_hooks.categories_filter_instructions"
|
||||
}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<CategorySelector
|
||||
@categories={{field.value}}
|
||||
@onChange={{field.set}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</form.Field>
|
||||
|
||||
{{#if this.showTagsFilter}}
|
||||
<form.Field
|
||||
@name="tag_names"
|
||||
@title={{i18n "admin.web_hooks.tags_filter"}}
|
||||
@description={{i18n
|
||||
"admin.web_hooks.tags_filter_instructions"
|
||||
}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<TagChooser
|
||||
@tags={{field.value}}
|
||||
@everyTag={{true}}
|
||||
@excludeSynonyms={{true}}
|
||||
@onChange={{field.set}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</form.Field>
|
||||
{{/if}}
|
||||
|
||||
<form.Field
|
||||
@name="group_names"
|
||||
@title={{i18n "admin.web_hooks.groups_filter"}}
|
||||
@description={{i18n
|
||||
"admin.web_hooks.groups_filter_instructions"
|
||||
}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<GroupSelector
|
||||
@groupNames={{field.value}}
|
||||
@groupFinder={{this.webhook.groupFinder}}
|
||||
@onChange={{field.set}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</form.Field>
|
||||
|
||||
<span>
|
||||
<PluginOutlet
|
||||
@name="web-hook-fields"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash model=this.webhook}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<form.Field
|
||||
@name="verify_certificate"
|
||||
@title={{i18n "admin.web_hooks.verify_certificate"}}
|
||||
@showTitle={{false}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="active"
|
||||
@title={{i18n "admin.web_hooks.active"}}
|
||||
@showTitle={{false}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</form.Field>
|
||||
|
||||
<form.Actions>
|
||||
<form.Submit class="save" @label={{this.saveButtonLabel}} />
|
||||
<form.Button
|
||||
@route="adminWebHooks.index"
|
||||
@label="admin.web_hooks.cancel"
|
||||
class="btn-default"
|
||||
/>
|
||||
</form.Actions>
|
||||
</Form>
|
||||
</ConditionalLoadingSection>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
|
||||
import WebhookItem from "admin/components/webhook-item";
|
||||
|
||||
export default class AdminConfigAreasWebhooksList extends Component {
|
||||
@service dialog;
|
||||
|
||||
@tracked webhooks = this.args.webhooks;
|
||||
|
||||
@action
|
||||
destroyWebhook(webhook) {
|
||||
return this.dialog.deleteConfirm({
|
||||
message: i18n("admin.web_hooks.delete_confirm"),
|
||||
didConfirm: async () => {
|
||||
try {
|
||||
await webhook.destroyRecord();
|
||||
this.webhooks.removeObject(webhook);
|
||||
} catch (e) {
|
||||
popupAjaxError(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="container admin-api_keys">
|
||||
{{#if this.webhooks}}
|
||||
<table class="d-admin-table admin-web_hooks__items">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n "admin.web_hooks.delivery_status.title"}}</th>
|
||||
<th>{{i18n "admin.web_hooks.payload_url"}}</th>
|
||||
<th>{{i18n "admin.web_hooks.description_label"}}</th>
|
||||
<th>{{i18n "admin.web_hooks.controls"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.webhooks as |webhook|}}
|
||||
<WebhookItem
|
||||
@webhook={{webhook}}
|
||||
@deliveryStatuses={{this.webhooks.extras.delivery_statuses}}
|
||||
@destroy={{this.destroyWebhook}}
|
||||
/>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<AdminConfigAreaEmptyList
|
||||
@ctaLabel="admin.web_hooks.add"
|
||||
@ctaRoute="adminWebHooks.new"
|
||||
@ctaClass="admin-web_hooks__add-web_hook"
|
||||
@emptyLabel="admin.web_hooks.none"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import WebhookStatus from "admin/components/webhook-status";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
|
||||
export default class WebhookItem extends Component {
|
||||
@service router;
|
||||
|
||||
@tracked webhook = this.args.webhook;
|
||||
deliveryStatuses = this.args.deliveryStatuses;
|
||||
|
||||
@action
|
||||
onRegisterApi(api) {
|
||||
this.dMenu = api;
|
||||
}
|
||||
|
||||
@action
|
||||
edit() {
|
||||
this.router.transitionTo("adminWebHooks.edit", this.webhook);
|
||||
}
|
||||
|
||||
@action
|
||||
events() {
|
||||
this.router.transitionTo("adminWebHooks.show", this.webhook);
|
||||
}
|
||||
|
||||
<template>
|
||||
<tr class="d-admin-row__content">
|
||||
<td class="d-admin-row__overview key">
|
||||
<LinkTo @route="adminWebHooks.show" @model={{this.webhook}}>
|
||||
<WebhookStatus
|
||||
@deliveryStatuses={{this.deliveryStatuses}}
|
||||
@webhook={{this.webhook}}
|
||||
/>
|
||||
</LinkTo>
|
||||
</td>
|
||||
<td class="d-admin-row__detail">
|
||||
<LinkTo @route="adminWebHooks.edit" @model={{this.webhook}}>
|
||||
{{this.webhook.payload_url}}
|
||||
</LinkTo>
|
||||
</td>
|
||||
<td class="d-admin-row__detail">
|
||||
{{this.webhook.description}}
|
||||
</td>
|
||||
<td class="d-admin-row__controls key-controls">
|
||||
<div class="d-admin-row__controls-options">
|
||||
<DButton
|
||||
@action={{this.edit}}
|
||||
@label="admin.web_hooks.edit"
|
||||
@title="admin.api.show_details"
|
||||
class="btn-small"
|
||||
/>
|
||||
<DMenu
|
||||
@identifier="webhook-menu"
|
||||
@title={{i18n "admin.config_areas.user_fields.more_options.title"}}
|
||||
@icon="ellipsis-vertical"
|
||||
@onRegisterApi={{this.onRegisterApi}}
|
||||
>
|
||||
<:content>
|
||||
<DropdownMenu as |dropdown|>
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@action={{this.events}}
|
||||
@icon="list"
|
||||
@label="admin.web_hooks.show"
|
||||
@title="admin.web_hooks.show"
|
||||
class="admin-web-hook__show"
|
||||
/>
|
||||
</dropdown.item>
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@action={{fn @destroy this.webhook}}
|
||||
@icon="trash-can"
|
||||
@label="admin.web_hooks.delete"
|
||||
@title="admin.web_hooks.delete"
|
||||
class="btn-danger admin-web-hook__delete"
|
||||
/>
|
||||
</dropdown.item>
|
||||
</DropdownMenu>
|
||||
</:content>
|
||||
</DMenu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
}
|
@ -41,7 +41,7 @@ export default class WebHook extends RestModel {
|
||||
verify_certificate = true;
|
||||
active = false;
|
||||
web_hook_event_types = null;
|
||||
groupsFilterInName = null;
|
||||
group_names = null;
|
||||
|
||||
@computed("wildcard_web_hook")
|
||||
get wildcard() {
|
||||
@ -73,7 +73,7 @@ export default class WebHook extends RestModel {
|
||||
updateGroupsFilter() {
|
||||
const groupIds = this.group_ids;
|
||||
this.set(
|
||||
"groupsFilterInName",
|
||||
"group_names",
|
||||
Site.currentProp("groups").reduce((groupNames, g) => {
|
||||
if (groupIds.includes(g.id)) {
|
||||
groupNames.push(g.name);
|
||||
@ -106,7 +106,7 @@ export default class WebHook extends RestModel {
|
||||
|
||||
// Hack as {{group-selector}} accepts a comma-separated string as data source, but
|
||||
// we use an array to populate the datasource above.
|
||||
const groupsFilter = this.groupsFilterInName;
|
||||
const groupsFilter = this.group_names;
|
||||
const groupNames =
|
||||
typeof groupsFilter === "string" ? groupsFilter.split(",") : groupsFilter;
|
||||
|
||||
|
@ -130,6 +130,7 @@ export default function () {
|
||||
"adminWebHooks",
|
||||
{ path: "/web_hooks", resetNamespace: true },
|
||||
function () {
|
||||
this.route("new");
|
||||
this.route("show", { path: "/:web_hook_id" });
|
||||
this.route("edit", { path: "/:web_hook_id/edit" });
|
||||
}
|
||||
|
@ -1,28 +1,7 @@
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminWebHooksEditRoute extends DiscourseRoute {
|
||||
serialize(model) {
|
||||
return { web_hook_id: model.id || "new" };
|
||||
}
|
||||
|
||||
model(params) {
|
||||
if (params.web_hook_id === "new") {
|
||||
return this.store.createRecord("web-hook");
|
||||
}
|
||||
|
||||
return this.store.find("web-hook", params.web_hook_id);
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
super.setupController(...arguments);
|
||||
|
||||
if (model.get("isNew")) {
|
||||
model.set(
|
||||
"web_hook_event_types",
|
||||
this.controllerFor("adminWebHooks").defaultEventTypes
|
||||
);
|
||||
}
|
||||
|
||||
controller.set("saved", false);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
import Route from "@ember/routing/route";
|
||||
|
||||
export default class AdminWebHooksIndexRoute extends Route {
|
||||
model() {
|
||||
return this.store.findAll("web-hook");
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminWebHooksNewRoute extends DiscourseRoute {
|
||||
model() {
|
||||
return this.store.createRecord("web-hook");
|
||||
}
|
||||
}
|
@ -1,17 +1,3 @@
|
||||
import Route from "@ember/routing/route";
|
||||
|
||||
export default class AdminWebHooksRoute extends Route {
|
||||
model() {
|
||||
return this.store.findAll("web-hook");
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.setProperties({
|
||||
model,
|
||||
groupedEventTypes: model.extras.grouped_event_types,
|
||||
defaultEventTypes: model.extras.default_event_types,
|
||||
contentTypes: model.extras.content_types,
|
||||
deliveryStatuses: model.extras.delivery_statuses,
|
||||
});
|
||||
}
|
||||
}
|
||||
export default class AdminWebHooksRoute extends Route {}
|
||||
|
@ -1,179 +1 @@
|
||||
<LinkTo @route="adminWebHooks" class="go-back">
|
||||
{{d-icon "arrow-left"}}
|
||||
{{i18n "admin.web_hooks.go_back"}}
|
||||
</LinkTo>
|
||||
|
||||
<div class="web-hook-container">
|
||||
<p>{{i18n "admin.web_hooks.detailed_instruction"}}</p>
|
||||
|
||||
<form class="web-hook form-horizontal">
|
||||
<div class="control-group">
|
||||
<label for="payload-url">{{i18n "admin.web_hooks.payload_url"}}</label>
|
||||
<TextField
|
||||
@name="payload-url"
|
||||
@value={{this.model.payload_url}}
|
||||
@placeholderKey="admin.web_hooks.payload_url_placeholder"
|
||||
/>
|
||||
<InputTip @validation={{this.urlValidation}} />
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="content-type">{{i18n "admin.web_hooks.content_type"}}</label>
|
||||
<ComboBox
|
||||
@content={{this.contentTypes}}
|
||||
@name="content-type"
|
||||
@value={{this.model.content_type}}
|
||||
@onChange={{fn (mut this.model.content_type)}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="secret">{{i18n "admin.web_hooks.secret"}}</label>
|
||||
<TextField
|
||||
@name="secret"
|
||||
@value={{this.model.secret}}
|
||||
@placeholderKey="admin.web_hooks.secret_placeholder"
|
||||
/>
|
||||
<InputTip @validation={{this.secretValidation}} />
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>{{i18n "admin.web_hooks.event_chooser"}}</label>
|
||||
|
||||
<label class="subscription-choice">
|
||||
<RadioButton
|
||||
@name="subscription-choice"
|
||||
@onChange={{fn (mut this.model.wildcard_web_hook) false}}
|
||||
@value={{false}}
|
||||
@selection={{this.model.wildcard_web_hook}}
|
||||
/>
|
||||
{{i18n "admin.web_hooks.individual_event"}}
|
||||
<InputTip @validation={{this.eventTypeValidation}} />
|
||||
</label>
|
||||
|
||||
{{#unless this.model.wildcard_web_hook}}
|
||||
<div class="event-selector">
|
||||
{{#each-in this.groupedEventTypes as |group eventTypes|}}
|
||||
<div class="event-group">
|
||||
{{i18n (concat "admin.web_hooks." group "_event.group_name")}}
|
||||
{{#each eventTypes as |type|}}
|
||||
<WebhookEventChooser
|
||||
@type={{type}}
|
||||
@group={{group}}
|
||||
@eventTypes={{this.model.web_hook_event_types}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each-in}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<label class="subscription-choice">
|
||||
<RadioButton
|
||||
@name="subscription-choice"
|
||||
@onChange={{fn (mut this.model.wildcard_web_hook) true}}
|
||||
@value={{true}}
|
||||
@selection={{this.model.wildcard_web_hook}}
|
||||
/>
|
||||
{{i18n "admin.web_hooks.wildcard_event"}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="filters control-group">
|
||||
<div class="filter">
|
||||
<label>{{d-icon "circle" class="tracking"}}{{i18n
|
||||
"admin.web_hooks.categories_filter"
|
||||
}}</label>
|
||||
<CategorySelector
|
||||
@categories={{this.model.categories}}
|
||||
@onChange={{fn (mut this.model.categories)}}
|
||||
/>
|
||||
<div class="instructions">{{i18n
|
||||
"admin.web_hooks.categories_filter_instructions"
|
||||
}}</div>
|
||||
</div>
|
||||
|
||||
{{#if this.showTagsFilter}}
|
||||
<div class="filter">
|
||||
<label>{{d-icon "circle" class="tracking"}}{{i18n
|
||||
"admin.web_hooks.tags_filter"
|
||||
}}</label>
|
||||
<TagChooser
|
||||
@tags={{this.model.tag_names}}
|
||||
@everyTag={{true}}
|
||||
@excludeSynonyms={{true}}
|
||||
/>
|
||||
<div class="instructions">{{i18n
|
||||
"admin.web_hooks.tags_filter_instructions"
|
||||
}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="filter">
|
||||
<label>{{d-icon "circle" class="tracking"}}{{i18n
|
||||
"admin.web_hooks.groups_filter"
|
||||
}}</label>
|
||||
<GroupSelector
|
||||
@groupNames={{this.model.groupsFilterInName}}
|
||||
@groupFinder={{this.model.groupFinder}}
|
||||
/>
|
||||
<div class="instructions">{{i18n
|
||||
"admin.web_hooks.groups_filter_instructions"
|
||||
}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
<PluginOutlet
|
||||
@name="web-hook-fields"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash model=this.model}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<label>
|
||||
<Input
|
||||
@type="checkbox"
|
||||
name="verify_certificate"
|
||||
@checked={{this.model.verify_certificate}}
|
||||
/>
|
||||
{{i18n "admin.web_hooks.verify_certificate"}}
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<label class="checkbox-label">
|
||||
<Input @type="checkbox" name="active" @checked={{this.model.active}} />
|
||||
{{i18n "admin.web_hooks.active"}}
|
||||
</label>
|
||||
|
||||
{{#if this.model.active}}
|
||||
<div class="instructions">{{i18n "admin.web_hooks.active_notice"}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="controls">
|
||||
<DButton
|
||||
@translatedLabel={{this.saveButtonText}}
|
||||
@action={{this.save}}
|
||||
@disabled={{this.saveButtonDisabled}}
|
||||
class="btn-primary admin-webhooks__save-button"
|
||||
/>
|
||||
|
||||
{{#if this.model.isNew}}
|
||||
<LinkTo @route="adminWebHooks" class="btn btn-default">
|
||||
{{i18n "cancel"}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<LinkTo
|
||||
@route="adminWebHooks.show"
|
||||
@model={{this.model}}
|
||||
class="btn btn-default"
|
||||
>
|
||||
{{i18n "cancel"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
||||
<span class="saving">{{this.savingStatus}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<AdminConfigAreas::WebhooksForm @webhook={{this.model}} />
|
@ -1,80 +1 @@
|
||||
<div class="web-hooks-listing">
|
||||
<p>{{i18n "admin.web_hooks.instruction"}}</p>
|
||||
|
||||
<div class="new-webhook">
|
||||
<LinkTo
|
||||
@route="adminWebHooks.edit"
|
||||
@model="new"
|
||||
class="btn btn-default admin-webhooks__new-button"
|
||||
>
|
||||
{{d-icon "plus"}}
|
||||
{{i18n "admin.web_hooks.new"}}
|
||||
</LinkTo>
|
||||
</div>
|
||||
|
||||
{{#if this.model}}
|
||||
<LoadMore @selector=".web-hooks tr" @action={{this.loadMore}}>
|
||||
<table class="d-admin-table web-hooks">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n "admin.web_hooks.payload_url"}}</th>
|
||||
<th>{{i18n "admin.web_hooks.description_label"}}</th>
|
||||
<th>{{i18n "admin.web_hooks.delivery_status.title"}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |webhook|}}
|
||||
<tr class="d-admin-row__content">
|
||||
<td class="d-admin-row__overview payload-url">
|
||||
<LinkTo @route="adminWebHooks.edit" @model={{webhook}}>
|
||||
{{webhook.payload_url}}
|
||||
</LinkTo>
|
||||
</td>
|
||||
<td class="d-admin-row__detail description">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "admin.web_hooks.description_label"}}
|
||||
</div>
|
||||
{{webhook.description}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail delivery-status">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "admin.web_hooks.delivery_status.title"}}
|
||||
</div>
|
||||
|
||||
<LinkTo @route="adminWebHooks.show" @model={{webhook}}>
|
||||
<WebhookStatus
|
||||
@deliveryStatuses={{this.deliveryStatuses}}
|
||||
@webhook={{webhook}}
|
||||
/>
|
||||
</LinkTo>
|
||||
</td>
|
||||
<td class="d-admin-row__controls controls">
|
||||
<div class="d-admin-row__controls-options">
|
||||
<LinkTo
|
||||
@route="adminWebHooks.edit"
|
||||
@model={{webhook}}
|
||||
class="btn btn-default no-text"
|
||||
title={{i18n "admin.web_hooks.edit"}}
|
||||
>
|
||||
{{d-icon "far-pen-to-square"}}
|
||||
</LinkTo>
|
||||
|
||||
<DButton
|
||||
@action={{fn this.destroyWebhook webhook}}
|
||||
@icon="xmark"
|
||||
@title="delete"
|
||||
class="destroy btn-danger"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
<ConditionalLoadingSpinner @condition={{this.model.loadingMore}} />
|
||||
</LoadMore>
|
||||
{{else}}
|
||||
<p>{{i18n "admin.web_hooks.none"}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<AdminConfigAreas::WebhooksList @webhooks={{this.model}} />
|
@ -0,0 +1 @@
|
||||
<AdminConfigAreas::WebhooksForm @webhook={{this.model}} />
|
@ -1,6 +1,6 @@
|
||||
<LinkTo @route="adminWebHooks" class="go-back">
|
||||
{{d-icon "arrow-left"}}
|
||||
{{i18n "admin.web_hooks.go_back"}}
|
||||
{{i18n "admin.web_hooks.back"}}
|
||||
</LinkTo>
|
||||
|
||||
<div class="admin-webhooks__summary">
|
||||
|
35
app/assets/javascripts/admin/addon/templates/web-hooks.gjs
Normal file
35
app/assets/javascripts/admin/addon/templates/web-hooks.gjs
Normal file
@ -0,0 +1,35 @@
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
||||
import DPageHeader from "discourse/components/d-page-header";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default RouteTemplate(<template>
|
||||
<div class="admin-webhooks admin-config-page">
|
||||
<DPageHeader
|
||||
@titleLabel={{i18n "admin.web_hooks.title"}}
|
||||
@descriptionLabel={{i18n "admin.web_hooks.instruction"}}
|
||||
@hideTabs={{true}}
|
||||
>
|
||||
<:breadcrumbs>
|
||||
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
|
||||
<DBreadcrumbsItem
|
||||
@path="/admin/api/web_hooks"
|
||||
@label={{i18n "admin.web_hooks.title"}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
<:actions as |actions|>
|
||||
<actions.Primary
|
||||
@route="adminWebHooks.new"
|
||||
@label="admin.web_hooks.add"
|
||||
/>
|
||||
</:actions>
|
||||
</DPageHeader>
|
||||
|
||||
<div class="admin-container admin-config-page__main-area">
|
||||
<PluginOutlet @name="admin-web-hooks">
|
||||
{{outlet}}
|
||||
</PluginOutlet>
|
||||
</div>
|
||||
</div>
|
||||
</template>);
|
@ -1,3 +0,0 @@
|
||||
<PluginOutlet @name="admin-web-hooks">
|
||||
{{outlet}}
|
||||
</PluginOutlet>
|
@ -38,7 +38,9 @@ export default class GroupSelector extends Component {
|
||||
onChangeItems: (items) => {
|
||||
selectedGroups = items;
|
||||
|
||||
if (this.onChangeCallback) {
|
||||
if (this.onChange) {
|
||||
this.onChange(items.join(","));
|
||||
} else if (this.onChangeCallback) {
|
||||
this.onChangeCallback(this.groupNames, selectedGroups);
|
||||
} else {
|
||||
this.set("groupNames", items.join(","));
|
||||
|
@ -1,78 +0,0 @@
|
||||
import { click, currentURL, fillIn, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import pretender, {
|
||||
parsePostData,
|
||||
response,
|
||||
} from "discourse/tests/helpers/create-pretender";
|
||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
|
||||
acceptance("Admin - Webhooks", function (needs) {
|
||||
needs.user();
|
||||
|
||||
test("adding a webhook", async function (assert) {
|
||||
pretender.get("/admin/api/web_hooks", () => {
|
||||
return response({
|
||||
web_hooks: [],
|
||||
total_rows_web_hooks: 0,
|
||||
load_more_web_hooks: "/admin/api/web_hooks.json?limit=50&offset=50",
|
||||
extras: {
|
||||
categories: [],
|
||||
content_types: [
|
||||
{ id: 1, name: "application/json" },
|
||||
{ id: 2, name: "application/x-www-form-urlencoded" },
|
||||
],
|
||||
default_event_types: [{ id: 2, name: "post" }],
|
||||
delivery_statuses: [
|
||||
{ id: 1, name: "inactive" },
|
||||
{ id: 2, name: "failed" },
|
||||
{ id: 3, name: "successful" },
|
||||
],
|
||||
event_types: [
|
||||
{ id: 1, name: "topic" },
|
||||
{ id: 2, name: "post" },
|
||||
{ id: 3, name: "user" },
|
||||
{ id: 4, name: "group" },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
pretender.get("/admin/api/web_hook_events/1", () => {
|
||||
return response({
|
||||
web_hook_events: [],
|
||||
load_more_web_hook_events:
|
||||
"/admin/api/web_hook_events/1.json?limit=50&offset=50",
|
||||
total_rows_web_hook_events: 15,
|
||||
extras: { web_hook_id: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
pretender.post("/admin/api/web_hooks", (request) => {
|
||||
const data = parsePostData(request.requestBody);
|
||||
assert.strictEqual(
|
||||
data.web_hook.payload_url,
|
||||
"https://example.com/webhook"
|
||||
);
|
||||
|
||||
return response({
|
||||
web_hook: {
|
||||
id: 1,
|
||||
// other attrs
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await visit("/admin/api/web_hooks");
|
||||
await click(".admin-webhooks__new-button");
|
||||
|
||||
await fillIn(`[name="payload-url"`, "https://example.com/webhook");
|
||||
|
||||
const categorySelector = selectKit(".category-selector");
|
||||
await categorySelector.expand();
|
||||
categorySelector.selectRowByName("support");
|
||||
|
||||
await click(".admin-webhooks__save-button");
|
||||
assert.strictEqual(currentURL(), "/admin/api/web_hooks/1");
|
||||
});
|
||||
});
|
@ -5542,27 +5542,30 @@ en:
|
||||
|
||||
web_hooks:
|
||||
title: "Webhooks"
|
||||
none: "There are no webhooks right now."
|
||||
none: "You don't have any webhooks yet"
|
||||
instruction: "Webhooks allows Discourse to notify external services when certain event happens in your site. When the webhook is triggered, a POST request will send to URLs provided."
|
||||
detailed_instruction: "A POST request will be sent to provided URL when chosen event happens."
|
||||
new: "New Webhook"
|
||||
add: "Add webhook"
|
||||
create: "Create"
|
||||
edit: "Edit"
|
||||
save: "Save"
|
||||
show: "Events"
|
||||
delete: "Delete"
|
||||
cancel: "Cancel"
|
||||
description_label: "Event triggers"
|
||||
controls: "Controls"
|
||||
go_back: "Back to list"
|
||||
back: "Back to webhooks"
|
||||
payload_url: "Payload URL"
|
||||
payload_url_placeholder: "https://example.com/postreceive"
|
||||
secret_invalid: "Secret must not have any blank characters."
|
||||
secret_too_short: "Secret should be at least 12 characters."
|
||||
secret_placeholder: "An optional string, used for generating signature"
|
||||
secret_placeholder: "String used for generating signatures"
|
||||
event_type_missing: "You need to set up at least one event type."
|
||||
content_type: "Content Type"
|
||||
secret: "Secret"
|
||||
event_chooser: "Which events should trigger this webhook?"
|
||||
wildcard_event: "Send me everything."
|
||||
individual_event: "Select individual events."
|
||||
wildcard_event: "Send me everything"
|
||||
individual_event: "Select individual events"
|
||||
verify_certificate: "Check TLS certificate of payload url"
|
||||
active: "Active"
|
||||
active_notice: "We will deliver event details when it happens."
|
||||
|
48
spec/system/admin_webhooks_spec.rb
Normal file
48
spec/system/admin_webhooks_spec.rb
Normal file
@ -0,0 +1,48 @@
|
||||
#frozen_string_literal: true
|
||||
|
||||
describe "Admin Webhooks Page", type: :system do
|
||||
fab!(:admin)
|
||||
|
||||
let(:webhooks_page) { PageObjects::Pages::AdminWebhooks.new }
|
||||
let(:dialog) { PageObjects::Components::Dialog.new }
|
||||
|
||||
before do
|
||||
Fabricate(:web_hook, payload_url: "https://www.example.com/1")
|
||||
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
it "shows a list of webhooks" do
|
||||
webhooks_page.visit_page
|
||||
|
||||
expect(webhooks_page).to have_webhook_listed("https://www.example.com/1")
|
||||
end
|
||||
|
||||
it "can add a new webhook" do
|
||||
webhooks_page.visit_page
|
||||
webhooks_page.add_webhook(payload_url: "https://www.example.com/2")
|
||||
|
||||
expect(webhooks_page).to have_webhook_details("https://www.example.com/2")
|
||||
|
||||
webhooks_page.click_back
|
||||
|
||||
expect(webhooks_page).to have_webhook_listed("https://www.example.com/2")
|
||||
end
|
||||
|
||||
it "can edit existing webhooks" do
|
||||
webhooks_page.visit_page
|
||||
webhooks_page.click_edit("https://www.example.com/1")
|
||||
webhooks_page.edit_payload_url("https://www.example.com/3")
|
||||
|
||||
expect(webhooks_page).to have_webhook_listed("https://www.example.com/3")
|
||||
end
|
||||
|
||||
it "can delete webhooks" do
|
||||
webhooks_page.visit_page
|
||||
webhooks_page.click_delete("https://www.example.com/1")
|
||||
|
||||
dialog.click_danger
|
||||
|
||||
expect(webhooks_page).to have_no_webhook_listed("https://www.example.com/1")
|
||||
end
|
||||
end
|
93
spec/system/page_objects/pages/admin_webhooks.rb
Normal file
93
spec/system/page_objects/pages/admin_webhooks.rb
Normal file
@ -0,0 +1,93 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class AdminWebhooks < PageObjects::Pages::Base
|
||||
def visit_page
|
||||
page.visit("/admin/api/web_hooks")
|
||||
self
|
||||
end
|
||||
|
||||
def has_webhook_listed?(url)
|
||||
page.has_css?(table_selector, text: url)
|
||||
end
|
||||
|
||||
def has_no_webhook_listed?(url)
|
||||
page.has_no_css?(table_selector, text: url)
|
||||
end
|
||||
|
||||
def has_webhook_details?(url)
|
||||
page.has_css?(summary_selector, text: url)
|
||||
end
|
||||
|
||||
def add_webhook(payload_url:)
|
||||
page.find(header_actions_selector, text: I18n.t("admin_js.admin.web_hooks.add")).click
|
||||
|
||||
form = page.find(form_selector)
|
||||
form.find(payload_url_field_selector).fill_in(with: payload_url)
|
||||
|
||||
click_save
|
||||
end
|
||||
|
||||
def edit_payload_url(payload_url)
|
||||
form = page.find(form_selector)
|
||||
form.find(payload_url_field_selector).fill_in(with: payload_url)
|
||||
|
||||
click_save
|
||||
end
|
||||
|
||||
def click_back
|
||||
page.find(back_button_selector).click
|
||||
end
|
||||
|
||||
def click_save
|
||||
page.find(save_button_selector).click
|
||||
end
|
||||
|
||||
def click_edit(payload_url)
|
||||
row = page.find(row_selector, text: payload_url)
|
||||
row.find("button", text: I18n.t("admin_js.admin.web_hooks.edit")).click
|
||||
end
|
||||
|
||||
def click_delete(payload_url)
|
||||
row = page.find(row_selector, text: payload_url)
|
||||
row.find(".webhook-menu-trigger").click
|
||||
page.find(".admin-web-hook__delete").click
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def table_selector
|
||||
".admin-web_hooks__items"
|
||||
end
|
||||
|
||||
def row_selector
|
||||
".d-admin-row__content"
|
||||
end
|
||||
|
||||
def form_selector
|
||||
".form-kit"
|
||||
end
|
||||
|
||||
def summary_selector
|
||||
".admin-webhooks__summary"
|
||||
end
|
||||
|
||||
def header_actions_selector
|
||||
".d-page-header__actions"
|
||||
end
|
||||
|
||||
def payload_url_field_selector
|
||||
"input[name='payload_url']"
|
||||
end
|
||||
|
||||
def save_button_selector
|
||||
".save"
|
||||
end
|
||||
|
||||
def back_button_selector
|
||||
".go-back"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user