222 lines
5.4 KiB
Ruby
222 lines
5.4 KiB
Ruby
# typed: strict
|
|
# frozen_string_literal: true
|
|
|
|
module GoodJobHelper
|
|
extend T::Sig
|
|
extend T::Helpers
|
|
abstract!
|
|
|
|
class AnsiSegment < T::Struct
|
|
include T::Struct::ActsAsComparable
|
|
|
|
prop :text, String
|
|
prop :class_names, T::Array[String], default: []
|
|
prop :url, T.nilable(String), default: nil
|
|
end
|
|
|
|
# ANSI escape code pattern
|
|
ANSI_PATTERN = /\e\[([0-9;]*)m/
|
|
UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
|
|
|
|
sig { params(text: String).returns(T::Array[AnsiSegment]) }
|
|
def parse_ansi(text)
|
|
segments = T.let([], T::Array[AnsiSegment])
|
|
current_classes = T::Array[String].new
|
|
|
|
# Split the text into parts based on ANSI codes
|
|
parts = text.split(ANSI_PATTERN)
|
|
|
|
# Process each part and its corresponding ANSI codes
|
|
parts.each_with_index do |part, index|
|
|
if index.even?
|
|
# This is text content
|
|
segments << AnsiSegment.new(
|
|
text: part,
|
|
class_names: current_classes.dup,
|
|
)
|
|
else
|
|
# This is an ANSI code
|
|
codes = part.split(";").map(&:to_i)
|
|
if codes == [0]
|
|
current_classes.clear
|
|
else
|
|
codes.each do |code|
|
|
class_name = ansi_code_to_class(code)
|
|
current_classes << class_name if class_name
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
segments.each_with_index do |s0, idx|
|
|
s1 = segments[idx + 1] || next
|
|
if s0.text == "[hle " && s1.text.match(/\d+/)
|
|
segments[idx + 1] = AnsiSegment.new(
|
|
text: s1.text,
|
|
class_names: s1.class_names,
|
|
url: Rails.application.routes.url_helpers.log_entry_path(s1.text),
|
|
)
|
|
end
|
|
end
|
|
|
|
# go through segments and detect UUIDs, splitting the segment at the uuid
|
|
# and adding them to the segments array. Should result in a <before>, <uuid>,
|
|
# <after> tuple.
|
|
segments.flat_map do |segment|
|
|
if (idx = segment.text.index(UUID_REGEX))
|
|
[
|
|
AnsiSegment.new(
|
|
text: segment.text[0...idx] || "",
|
|
class_names: segment.class_names,
|
|
),
|
|
AnsiSegment.new(
|
|
text: segment.text[idx...idx + 36] || "",
|
|
class_names: ["log-uuid"],
|
|
url: "/jobs/jobs/#{segment.text[idx...idx + 36]}",
|
|
),
|
|
AnsiSegment.new(
|
|
text: segment.text[idx + 36..] || "",
|
|
class_names: segment.class_names,
|
|
),
|
|
]
|
|
else
|
|
[segment]
|
|
end
|
|
end
|
|
end
|
|
|
|
class JobArg < T::Struct
|
|
const :key, Symbol
|
|
const :value, T.untyped
|
|
const :inferred, T::Boolean
|
|
end
|
|
|
|
sig { params(job: GoodJob::Job).returns(T::Array[JobArg]) }
|
|
def arguments_for_job(job)
|
|
begin
|
|
deserialized =
|
|
T.cast(
|
|
ActiveJob::Arguments.deserialize(job.serialized_params).to_h,
|
|
T::Hash[String, T.untyped],
|
|
)
|
|
rescue ActiveJob::DeserializationError => e
|
|
Rails.logger.error(
|
|
"error deserializing job arguments: #{e.class.name} - #{e.message}",
|
|
)
|
|
return [JobArg.new(key: :error, value: e.message, inferred: true)]
|
|
end
|
|
args_hash =
|
|
T.cast(deserialized["arguments"].first, T::Hash[Symbol, T.untyped])
|
|
args =
|
|
args_hash.map { |key, value| JobArg.new(key:, value:, inferred: false) }
|
|
|
|
if args_hash[:fa_id] && !args_hash[:post] &&
|
|
args << JobArg.new(
|
|
key: :post,
|
|
value:
|
|
Domain::Post::FaPost.find_by(fa_id: args_hash[:fa_id]) ||
|
|
"post not found",
|
|
inferred: true,
|
|
)
|
|
end
|
|
|
|
if args_hash[:url_name] && !args_hash[:user]
|
|
args << JobArg.new(
|
|
key: :user,
|
|
value:
|
|
Domain::User::FaUser.find_by(url_name: args_hash[:url_name]) ||
|
|
"user not found",
|
|
inferred: true,
|
|
)
|
|
end
|
|
|
|
if job_id = args_hash[:caused_by_job_id]
|
|
job = GoodJob::Job.find_by(id: job_id)
|
|
if job
|
|
args << JobArg.new(key: :caused_by_job, value: job, inferred: false)
|
|
args.delete_if { |arg| arg.key == :caused_by_job_id }
|
|
end
|
|
end
|
|
|
|
args.sort_by(&:key)
|
|
end
|
|
|
|
sig { params(state: String).returns(String) }
|
|
def post_file_state_badge_class(state)
|
|
case state
|
|
when "ok"
|
|
"bg-success"
|
|
when "terminal_error"
|
|
"bg-danger"
|
|
when "retryable_error"
|
|
"bg-warning text-dark"
|
|
when "pending"
|
|
"bg-info"
|
|
else
|
|
"bg-secondary"
|
|
end
|
|
end
|
|
|
|
sig { params(state: String).returns(String) }
|
|
def post_file_state_icon_class(state)
|
|
base =
|
|
case state
|
|
when "ok"
|
|
"fa-check"
|
|
when "terminal_error"
|
|
"fa-circle-xmark"
|
|
when "retryable_error"
|
|
"fa-triangle-exclamation"
|
|
when "pending"
|
|
"fa-clock"
|
|
else
|
|
"fa-question"
|
|
end
|
|
"fa-solid #{base}"
|
|
end
|
|
|
|
private
|
|
|
|
sig { params(code: Integer).returns(T.nilable(String)) }
|
|
def ansi_code_to_class(code)
|
|
case code
|
|
when 1
|
|
"ansi-bold"
|
|
when 30
|
|
"ansi-black"
|
|
when 31
|
|
"ansi-red"
|
|
when 32
|
|
"ansi-green"
|
|
when 33
|
|
"ansi-yellow"
|
|
when 34
|
|
"ansi-blue"
|
|
when 35
|
|
"ansi-magenta"
|
|
when 36
|
|
"ansi-cyan"
|
|
when 37
|
|
"ansi-white"
|
|
when 90
|
|
"ansi-bright-black"
|
|
when 91
|
|
"ansi-bright-red"
|
|
when 92
|
|
"ansi-bright-green"
|
|
when 93
|
|
"ansi-bright-yellow"
|
|
when 94
|
|
"ansi-bright-blue"
|
|
when 95
|
|
"ansi-bright-magenta"
|
|
when 96
|
|
"ansi-bright-cyan"
|
|
when 97
|
|
"ansi-bright-white"
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
end
|