diff --git a/src/opnsense/www/js/opnsense_ui.js b/src/opnsense/www/js/opnsense_ui.js
index 434009953..f5e3ddf61 100644
--- a/src/opnsense/www/js/opnsense_ui.js
+++ b/src/opnsense/www/js/opnsense_ui.js
@@ -470,3 +470,40 @@ stdDialogRemoveItem.defaults = {
'accept': 'Yes',
'decline': 'Cancel'
};
+
+
+/**
+ * Action button, expects the following data attributes on the widget
+ * data-endpoint='/path/to/my/endpoint'
+ * data-label="Apply text"
+ * data-service-widget="service" (optional service widget to signal)
+ * data-error-title="My error message"
+ */
+$.fn.SimpleActionButton = function (params) {
+ let this_button = this;
+ this.construct = function() {
+ let label_content = '' + this_button.data('label') + ' ';
+ this_button.html(label_content);
+ this_button.on('click', function(){
+ this_button.find('.reload_progress').addClass("fa fa-spinner fa-pulse");
+ ajaxCall(this_button.data('endpoint'), {}, function(data,status) {
+ if (status != "success" || data['status'] != 'ok') {
+ BootstrapDialog.show({
+ type: BootstrapDialog.TYPE_WARNING,
+ title: this_button.data('error-title'),
+ message: data['status'],
+ draggable: true
+ });
+ }
+ this_button.find('.reload_progress').removeClass("fa fa-spinner fa-pulse");
+ if (this_button.data('service-widget')) {
+ updateServiceControlUI(this_button.data('service-widget'));
+ }
+ });
+ });
+ }
+ return this.each(function(){
+ const button = this_button.construct();
+ return button;
+ });
+}