Sam 8c8bc94ed8
FEATURE: Add automation statistics tracking to Automation (#31921)
introduces comprehensive statistics tracking for the Discourse
Automation plugin, allowing users to monitor the performance and
execution patterns of their automations:

- Add `discourse_automation_stats` table to track execution metrics
including run counts, execution times, and performance data
- Create a new `Stat` model to handle tracking and retrieving automation
statistics
- Update the admin UI to display automation stats (runs today/this
week/month and last run time)
- Modernize the automation list interface using Glimmer components
- Replace the older enable/disable icon with a toggle switch for better
UX
- Add schema annotations to existing models for better code
documentation
- Include extensive test coverage for the new statistics functionality

This helps administrators understand how their automations are
performing and identify potential bottlenecks or optimization
opportunities.

---------

Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: Ted Johansson <ted@discourse.org>
2025-03-21 12:53:26 +11:00

267 lines
6.0 KiB
Ruby

# frozen_string_literal: true
module DiscourseAutomation
class Field < ActiveRecord::Base
self.table_name = "discourse_automation_fields"
belongs_to :automation, class_name: "DiscourseAutomation::Automation"
around_save :on_update_callback
def on_update_callback
previous_fields = automation.serialized_fields
automation.reset!
yield
automation&.triggerable&.on_update&.call(
automation,
automation.serialized_fields,
previous_fields,
)
end
validate :required_field
def required_field
if template && template[:required] && metadata && metadata["value"].blank?
raise_required_field(name, target, targetable)
end
end
validate :validator
def validator
if template && template[:validator]
error = template[:validator].call(metadata["value"])
errors.add(:base, error) if error
end
end
def targetable
target == "trigger" ? automation.triggerable : automation.scriptable
end
def template
targetable&.fields&.find do |tf|
targetable.id == target && tf[:name].to_s == name && tf[:component].to_s == component
end
end
validate :metadata_schema
def metadata_schema
if !(targetable.components.include?(component.to_sym))
errors.add(
:base,
I18n.t(
"discourse_automation.models.fields.invalid_field",
component: component,
target: target,
target_name: targetable.name,
),
)
else
schema = SCHEMAS[component]
if !schema ||
!JSONSchemer.schema({ "type" => "object", "properties" => schema }).valid?(metadata)
errors.add(
:base,
I18n.t(
"discourse_automation.models.fields.invalid_metadata",
component: component,
field: name,
),
)
end
end
end
SCHEMAS = {
"key-value" => {
"type" => "array",
"uniqueItems" => true,
"items" => {
"type" => "object",
"title" => "group",
"properties" => {
"key" => {
"type" => "string",
},
"value" => {
"type" => "string",
},
},
},
},
"choices" => {
"value" => {
"type" => %w[string integer null array],
},
},
"tags" => {
"value" => {
"type" => "array",
"items" => [{ type: "string" }],
},
},
"trust-levels" => {
"value" => {
"type" => "array",
"items" => [{ type: "integer" }],
},
},
"categories" => {
"value" => {
"type" => "array",
"items" => [{ type: "string" }],
},
},
"category" => {
"value" => {
"type" => %w[string integer null],
},
},
"category_notification_level" => {
"value" => {
"type" => "integer",
},
},
"custom_field" => {
"value" => {
"type" => "integer",
},
},
"custom_fields" => {
"value" => {
"type" => [{ type: "string" }],
},
},
"user" => {
"value" => {
"type" => "string",
},
},
"user_profile" => {
"value" => {
"type" => "array",
"items" => [{ type: "string" }],
},
},
"users" => {
"value" => {
"type" => "array",
"items" => [{ type: "string" }],
},
},
"text" => {
"value" => {
"type" => %w[string integer null],
},
},
"post" => {
"value" => {
"type" => %w[string integer null],
},
},
"message" => {
"value" => {
"type" => %w[string integer null],
},
},
"boolean" => {
"value" => {
"type" => ["boolean"],
},
},
"text_list" => {
"value" => {
"type" => "array",
"items" => [{ type: "string" }],
},
},
"date_time" => {
"value" => {
"type" => "string",
},
},
"group" => {
"value" => {
"type" => "integer",
},
},
"groups" => {
"value" => {
"type" => "array",
"items" => [{ type: "integer" }],
},
},
"email_group_user" => {
"value" => {
"type" => "array",
"items" => [{ type: "string" }],
},
},
"pms" => {
type: "array",
items: [
{
type: "object",
properties: {
"raw" => {
"type" => "string",
},
"title" => {
"type" => "string",
},
"delay" => {
"type" => "integer",
},
"prefers_encrypt" => {
"type" => "boolean",
},
},
},
],
},
"period" => {
"type" => "object",
"properties" => {
"interval" => {
"type" => "integer",
},
"frequency" => {
"type" => "string",
},
},
},
}
private
def raise_required_field(name, target, targetable)
errors.add(
:base,
I18n.t(
"discourse_automation.models.fields.required_field",
name: name,
target: target,
target_name: targetable.name,
),
)
end
end
end
# == Schema Information
#
# Table name: discourse_automation_fields
#
# id :bigint not null, primary key
# automation_id :bigint not null
# metadata :jsonb not null
# component :string not null
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
# target :string
#