karafka-web 0.11.0.rc2 → 0.11.0.rc3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/Gemfile.lock +5 -5
- data/config/locales/slogans.yml +1 -1
- data/docker-compose.yml +1 -1
- data/lib/karafka/web/pro/ui/controllers/errors_controller.rb +8 -4
- data/lib/karafka/web/pro/ui/controllers/explorer/explorer_controller.rb +19 -9
- data/lib/karafka/web/pro/ui/views/consumers/jobs/_job.erb +1 -2
- data/lib/karafka/web/pro/ui/views/consumers/jobs/_no_jobs.erb +2 -6
- data/lib/karafka/web/pro/ui/views/consumers/partitions/offsets/edit.erb +5 -1
- data/lib/karafka/web/pro/ui/views/consumers/partitions/pauses/edit.erb +5 -1
- data/lib/karafka/web/pro/ui/views/consumers/partitions/pauses/new.erb +5 -1
- data/lib/karafka/web/pro/ui/views/errors/_breadcrumbs.erb +1 -6
- data/lib/karafka/web/pro/ui/views/errors/_error.erb +6 -1
- data/lib/karafka/web/pro/ui/views/errors/show.erb +39 -33
- data/lib/karafka/web/pro/ui/views/explorer/explorer/show.erb +76 -71
- data/lib/karafka/web/pro/ui/views/jobs/_job.erb +1 -2
- data/lib/karafka/web/ui/base.rb +2 -0
- data/lib/karafka/web/ui/controllers/errors_controller.rb +12 -3
- data/lib/karafka/web/ui/helpers/application_helper.rb +0 -70
- data/lib/karafka/web/ui/helpers/time_helper.rb +82 -0
- data/lib/karafka/web/ui/helpers/topics_helper.rb +156 -0
- data/lib/karafka/web/ui/models/message.rb +20 -2
- data/lib/karafka/web/ui/public/stylesheets/application.min.css +10 -35
- data/lib/karafka/web/ui/public/stylesheets/application.min.css.br +0 -0
- data/lib/karafka/web/ui/public/stylesheets/application.min.css.gz +0 -0
- data/lib/karafka/web/ui/routes/errors.rb +5 -1
- data/lib/karafka/web/ui/views/consumers/_assignments_badges.erb +2 -7
- data/lib/karafka/web/ui/views/errors/_breadcrumbs.erb +3 -8
- data/lib/karafka/web/ui/views/errors/_error.erb +6 -1
- data/lib/karafka/web/ui/views/errors/show.erb +39 -33
- data/lib/karafka/web/ui/views/jobs/_job.erb +1 -2
- data/lib/karafka/web/ui/views/shared/_compacted_message_info.erb +16 -0
- data/lib/karafka/web/version.rb +1 -1
- data/package-lock.json +71 -71
- data/package.json +4 -4
- metadata +4 -1
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Web
|
5
|
+
module Ui
|
6
|
+
module Helpers
|
7
|
+
# Helper with time-related methods
|
8
|
+
module TimeHelper
|
9
|
+
# @param time [Float] UTC time float
|
10
|
+
# @return [String] relative time tag for timeago.js
|
11
|
+
def relative_time(time)
|
12
|
+
stamp = Time.at(time).getutc.iso8601(3)
|
13
|
+
%(<time class="ltr" dir="ltr" title="#{stamp}" datetime="#{stamp}">#{time}</time>)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param time [Time] time object we want to present with detailed ms label
|
17
|
+
# @return [String] span tag with raw timestamp as a title and time as a value
|
18
|
+
def time_with_label(time)
|
19
|
+
stamp = (time.to_f * 1000).to_i
|
20
|
+
|
21
|
+
%(<span title="#{stamp}">#{time}</span>)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Converts raw second count into human readable form like "12.2 minutes". etc based on
|
25
|
+
# number of seconds
|
26
|
+
#
|
27
|
+
# @param seconds [Numeric] number of seconds
|
28
|
+
# @return [String] human readable time
|
29
|
+
def human_readable_time(seconds)
|
30
|
+
case seconds
|
31
|
+
when 0..59
|
32
|
+
"#{seconds.round(2)} seconds"
|
33
|
+
when 60..3_599
|
34
|
+
minutes = seconds / 60.0
|
35
|
+
"#{minutes.round(2)} minutes"
|
36
|
+
when 3_600..86_399
|
37
|
+
hours = seconds / 3_600.0
|
38
|
+
"#{hours.round(2)} hours"
|
39
|
+
else
|
40
|
+
days = seconds / 86_400.0
|
41
|
+
"#{days.round(2)} days"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param state [String] poll state
|
46
|
+
# @param state_ch [Integer] time until next change of the poll state
|
47
|
+
# (from paused to active)
|
48
|
+
# @return [String] span tag with label and title with change time if present
|
49
|
+
def poll_state_with_change_time_label(state, state_ch)
|
50
|
+
year_in_seconds = 131_556_926
|
51
|
+
state_ch_in_seconds = state_ch / 1_000.0
|
52
|
+
|
53
|
+
# If state is active, there is no date of change
|
54
|
+
if state == 'active'
|
55
|
+
%(
|
56
|
+
<span class="badge #{kafka_state_badge(state)}">#{state}</span>
|
57
|
+
)
|
58
|
+
elsif state_ch_in_seconds > year_in_seconds
|
59
|
+
%(
|
60
|
+
<span
|
61
|
+
class="badge #{kafka_state_badge(state)}"
|
62
|
+
title="until manual resume"
|
63
|
+
>
|
64
|
+
#{state}
|
65
|
+
</span>
|
66
|
+
)
|
67
|
+
else
|
68
|
+
%(
|
69
|
+
<span
|
70
|
+
class="badge #{kafka_state_badge(state)} time-title"
|
71
|
+
title="#{Time.now + state_ch_in_seconds}"
|
72
|
+
>
|
73
|
+
#{state}
|
74
|
+
</span>
|
75
|
+
)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Karafka
|
4
|
+
module Web
|
5
|
+
module Ui
|
6
|
+
module Helpers
|
7
|
+
# Helper module for formatting Kafka topic and partition information
|
8
|
+
# in various contexts within the Karafka Web UI.
|
9
|
+
#
|
10
|
+
# This module provides consistent formatting for topic-partition assignments
|
11
|
+
# across different display contexts like inline text, labels, and identifiers.
|
12
|
+
#
|
13
|
+
# @see https://karafka.io/docs/Development-Naming-Conventions
|
14
|
+
module TopicsHelper
|
15
|
+
# Default limit for displaying partitions before truncation
|
16
|
+
DEFAULT_LIMIT = 5
|
17
|
+
|
18
|
+
# Formats topic and partitions for inline text display in views.
|
19
|
+
#
|
20
|
+
# This method is optimized for compact display in assignments, logs, and
|
21
|
+
# other inline contexts where space is limited.
|
22
|
+
#
|
23
|
+
# @param topic [String] the Kafka topic name
|
24
|
+
# @param partitions [Array<Integer>, Integer] partition number(s) to format
|
25
|
+
# @param limit [Integer] maximum number of partitions to display before truncation
|
26
|
+
# @return [String] formatted topic-partition string
|
27
|
+
#
|
28
|
+
# @example Single partition
|
29
|
+
# topics_assignment_text("user-events", 0)
|
30
|
+
# # => "user-events-[0]"
|
31
|
+
#
|
32
|
+
# @example Multiple consecutive partitions
|
33
|
+
# topics_assignment_text("user-events", [0, 1, 2, 3])
|
34
|
+
# # => "user-events-[0-3]"
|
35
|
+
#
|
36
|
+
# @example Multiple non-consecutive partitions
|
37
|
+
# topics_assignment_text("user-events", [0, 2, 4])
|
38
|
+
# # => "user-events-[0,2,4]"
|
39
|
+
#
|
40
|
+
# @example Truncated partitions list
|
41
|
+
# topics_assignment_text("user-events", [0, 1, 2, 3, 4, 5, 6], limit: 3)
|
42
|
+
# # => "user-events-[0,1,2...]"
|
43
|
+
#
|
44
|
+
# @example Empty partitions
|
45
|
+
# topics_assignment_text("user-events", [])
|
46
|
+
# # => "user-events"
|
47
|
+
def topics_assignment_text(topic, partitions, limit: DEFAULT_LIMIT)
|
48
|
+
partitions = Array(partitions)
|
49
|
+
|
50
|
+
if partitions.empty?
|
51
|
+
topic
|
52
|
+
elsif partitions.size == 1
|
53
|
+
"#{topic}-[#{partitions.first}]"
|
54
|
+
else
|
55
|
+
sorted_partitions = partitions.map(&:to_i).sort
|
56
|
+
# Check for consecutive first (best representation)
|
57
|
+
if topics_consecutive?(sorted_partitions) && sorted_partitions.size > 2
|
58
|
+
"#{topic}-[#{sorted_partitions.first}-#{sorted_partitions.last}]"
|
59
|
+
# Apply limit if specified and partitions exceed it
|
60
|
+
elsif limit && sorted_partitions.size > limit
|
61
|
+
displayed_partitions = sorted_partitions.first(limit)
|
62
|
+
"#{topic}-[#{displayed_partitions.join(',')}...]"
|
63
|
+
else
|
64
|
+
"#{topic}-[#{sorted_partitions.join(',')}]"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Formats topic and partitions for human-readable labels and headers.
|
70
|
+
#
|
71
|
+
# This method provides more descriptive formatting suitable for page titles,
|
72
|
+
# section headers, and other contexts where additional context is helpful.
|
73
|
+
#
|
74
|
+
# @param topic [String] the Kafka topic name
|
75
|
+
# @param partitions [Array<Integer>, Integer] partition number(s) to format
|
76
|
+
# @param limit [Integer] maximum number of partitions to display before truncation
|
77
|
+
# @return [String] formatted topic-partition label with additional context
|
78
|
+
#
|
79
|
+
# @example Consecutive partitions with count
|
80
|
+
# topics_assignment_label("user-events", [0, 1, 2, 3])
|
81
|
+
# # => "user-events-[0-3] (4 partitions total)"
|
82
|
+
#
|
83
|
+
# @example Truncated with remaining count
|
84
|
+
# topics_assignment_label("user-events", [0, 1, 2, 3, 4, 5], limit: 3)
|
85
|
+
# # => "user-events-[0,1,2] (+3 more)"
|
86
|
+
#
|
87
|
+
# @example Non-consecutive partitions
|
88
|
+
# topics_assignment_label("user-events", [0, 2, 4])
|
89
|
+
# # => "user-events-[0,2,4]"
|
90
|
+
def topics_assignment_label(topic, partitions, limit: DEFAULT_LIMIT)
|
91
|
+
partitions = Array(partitions)
|
92
|
+
|
93
|
+
sorted_partitions = partitions.map(&:to_i).sort
|
94
|
+
if topics_consecutive?(sorted_partitions)
|
95
|
+
"#{topic}-[#{sorted_partitions.first}-#{sorted_partitions.last}] " \
|
96
|
+
"(#{partitions.size} partitions total)"
|
97
|
+
elsif sorted_partitions.size > limit
|
98
|
+
displayed = sorted_partitions.first(limit)
|
99
|
+
remaining = sorted_partitions.size - limit
|
100
|
+
"#{topic}-[#{displayed.join(',')}] (+#{remaining} more)"
|
101
|
+
else
|
102
|
+
"#{topic}-[#{sorted_partitions.join(',')}]"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Creates a specific identifier for topic-partition combinations.
|
107
|
+
#
|
108
|
+
# This method generates consistent identifiers used in metrics collection,
|
109
|
+
# cache keys, and other contexts requiring unique topic-partition identification.
|
110
|
+
#
|
111
|
+
# @param topic [String] the Kafka topic name
|
112
|
+
# @param partition [Integer] the partition number
|
113
|
+
# @return [String] formatted topic-partition identifier
|
114
|
+
#
|
115
|
+
# @example Basic identifier
|
116
|
+
# topics_partition_identifier("user-events", 0)
|
117
|
+
# # => "user-events-0"
|
118
|
+
#
|
119
|
+
# @example Used for cache keys
|
120
|
+
# cache_key = topics_partition_identifier("orders", 3)
|
121
|
+
# Rails.cache.fetch(cache_key) { expensive_operation }
|
122
|
+
def topics_partition_identifier(topic, partition)
|
123
|
+
"#{topic}-#{partition}"
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# Checks if an array of sorted integers contains consecutive numbers.
|
129
|
+
#
|
130
|
+
# This helper method determines whether partition numbers form a continuous
|
131
|
+
# sequence, which allows for more compact display formatting.
|
132
|
+
#
|
133
|
+
# @param sorted_array [Array<Integer>] array of sorted integers to check
|
134
|
+
# @return [Boolean] true if all numbers are consecutive, false otherwise
|
135
|
+
#
|
136
|
+
# @example Consecutive numbers
|
137
|
+
# topics_consecutive?([1, 2, 3, 4]) # => true
|
138
|
+
#
|
139
|
+
# @example Non-consecutive numbers
|
140
|
+
# topics_consecutive?([1, 3, 5, 7]) # => false
|
141
|
+
#
|
142
|
+
# @example Single element (not consecutive)
|
143
|
+
# topics_consecutive?([1]) # => false
|
144
|
+
#
|
145
|
+
# @example Empty array (not consecutive)
|
146
|
+
# topics_consecutive?([]) # => false
|
147
|
+
def topics_consecutive?(sorted_array)
|
148
|
+
return false if sorted_array.size < 2
|
149
|
+
|
150
|
+
sorted_array.each_cons(2).all? { |a, b| b == a + 1 }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -10,13 +10,21 @@ module Karafka
|
|
10
10
|
extend Lib::Paginations::Paginators
|
11
11
|
|
12
12
|
class << self
|
13
|
-
# Looks for a message from a given topic partition
|
13
|
+
# Looks for a message from a given topic partition. When no offsets provided, will
|
14
|
+
# raise if there is no data under the given offset. If watermarks were provided, it
|
15
|
+
# will check if this is a system entry and in such cases will return nil.
|
16
|
+
# Will always raise if request is out of range.
|
14
17
|
#
|
15
18
|
# @param topic_id [String]
|
16
19
|
# @param partition_id [Integer]
|
17
20
|
# @param offset [Integer]
|
21
|
+
# @param watermark_offsets [WatermarkOffsets, false] watermark offsets for this topic
|
22
|
+
# partition or false if not provided
|
23
|
+
# @return [Karafka::Messages::Message, nil] found message or nil in case watermark
|
24
|
+
# offsets were provided and we encountered a message matching watermarks
|
18
25
|
# @raise [::Karafka::Web::Errors::Ui::NotFoundError] when not found
|
19
|
-
|
26
|
+
# @note If no watermark offsets provided will always raise if no message with data
|
27
|
+
def find(topic_id, partition_id, offset, watermark_offsets: false)
|
20
28
|
message = Lib::Admin.read_topic(
|
21
29
|
topic_id,
|
22
30
|
partition_id,
|
@@ -26,6 +34,16 @@ module Karafka
|
|
26
34
|
|
27
35
|
return message if message
|
28
36
|
|
37
|
+
# Not found can also occur for system entries and compacted messages.
|
38
|
+
# Since we want to know about this in some cases we handle this case and check if the
|
39
|
+
# requested offset is within the range and if so, it means it has been cleaned or
|
40
|
+
# is a system entry. In such cases we do display user an info message.
|
41
|
+
return nil if watermark_offsets &&
|
42
|
+
offset >= watermark_offsets.low &&
|
43
|
+
offset < watermark_offsets.high
|
44
|
+
|
45
|
+
# If beyond the watermark offsets, we raise 404 as user should not reach such
|
46
|
+
# non-existent messages as we cannot reason about them
|
29
47
|
raise(
|
30
48
|
::Karafka::Web::Errors::Ui::NotFoundError,
|
31
49
|
[topic_id, partition_id, offset].join(', ')
|
@@ -11,7 +11,7 @@
|
|
11
11
|
.air-datepicker{--adp-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--adp-font-size: 14px;--adp-width: 246px;--adp-z-index: 100;--adp-padding: 4px;--adp-grid-areas: "nav" "body" "timepicker" "buttons";--adp-transition-duration: .3s;--adp-transition-ease: ease-out;--adp-transition-offset: 8px;--adp-background-color: #fff;--adp-background-color-hover: #f0f0f0;--adp-background-color-active: #eaeaea;--adp-background-color-in-range: rgba(92, 196, 239, .1);--adp-background-color-in-range-focused: rgba(92, 196, 239, .2);--adp-background-color-selected-other-month-focused: #8ad5f4;--adp-background-color-selected-other-month: #a2ddf6;--adp-color: #4a4a4a;--adp-color-secondary: #9c9c9c;--adp-accent-color: #4eb5e6;--adp-color-current-date: var(--adp-accent-color);--adp-color-other-month: #dedede;--adp-color-disabled: #aeaeae;--adp-color-disabled-in-range: #939393;--adp-color-other-month-hover: #c5c5c5;--adp-border-color: #dbdbdb;--adp-border-color-inner: #efefef;--adp-border-radius: 4px;--adp-border-color-inline: #d7d7d7;--adp-nav-height: 32px;--adp-nav-arrow-color: var(--adp-color-secondary);--adp-nav-action-size: 32px;--adp-nav-color-secondary: var(--adp-color-secondary);--adp-day-name-color: #ff9a19;--adp-day-name-color-hover: #8ad5f4;--adp-day-cell-width: 1fr;--adp-day-cell-height: 32px;--adp-month-cell-height: 42px;--adp-year-cell-height: 56px;--adp-pointer-size: 10px;--adp-poiner-border-radius: 2px;--adp-pointer-offset: 14px;--adp-cell-border-radius: 4px;--adp-cell-background-color-hover: var(--adp-background-color-hover);--adp-cell-background-color-selected: #5cc4ef;--adp-cell-background-color-selected-hover: #45bced;--adp-cell-background-color-in-range: rgba(92, 196, 239, 0.1);--adp-cell-background-color-in-range-hover: rgba(92, 196, 239, 0.2);--adp-cell-border-color-in-range: var(--adp-cell-background-color-selected);--adp-btn-height: 32px;--adp-btn-color: var(--adp-accent-color);--adp-btn-color-hover: var(--adp-color);--adp-btn-border-radius: var(--adp-border-radius);--adp-btn-background-color-hover: var(--adp-background-color-hover);--adp-btn-background-color-active: var(--adp-background-color-active);--adp-time-track-height: 1px;--adp-time-track-color: #dedede;--adp-time-track-color-hover: #b1b1b1;--adp-time-thumb-size: 12px;--adp-time-padding-inner: 10px;--adp-time-day-period-color: var(--adp-color-secondary);--adp-mobile-font-size: 16px;--adp-mobile-nav-height: 40px;--adp-mobile-width: 320px;--adp-mobile-day-cell-height: 38px;--adp-mobile-month-cell-height: 48px;--adp-mobile-year-cell-height: 64px}.air-datepicker-overlay{--adp-overlay-background-color: rgba(0, 0, 0, .3);--adp-overlay-transition-duration: .3s;--adp-overlay-transition-ease: ease-out;--adp-overlay-z-index: 99}
|
12
12
|
.air-datepicker{background:var(--adp-background-color);border:1px solid var(--adp-border-color);box-shadow:0 4px 12px rgba(0,0,0,.15);border-radius:var(--adp-border-radius);box-sizing:content-box;display:grid;grid-template-columns:1fr;grid-template-rows:repeat(4, max-content);grid-template-areas:var(--adp-grid-areas);font-family:var(--adp-font-family),sans-serif;font-size:var(--adp-font-size);color:var(--adp-color);width:var(--adp-width);position:absolute;transition:opacity var(--adp-transition-duration) var(--adp-transition-ease),transform var(--adp-transition-duration) var(--adp-transition-ease);z-index:var(--adp-z-index)}.air-datepicker:not(.-custom-position-){opacity:0}.air-datepicker.-from-top-{transform:translateY(calc(var(--adp-transition-offset) * -1))}.air-datepicker.-from-right-{transform:translateX(var(--adp-transition-offset))}.air-datepicker.-from-bottom-{transform:translateY(var(--adp-transition-offset))}.air-datepicker.-from-left-{transform:translateX(calc(var(--adp-transition-offset) * -1))}.air-datepicker.-active-:not(.-custom-position-){transform:translate(0, 0);opacity:1}.air-datepicker.-active-.-custom-position-{transition:none}.air-datepicker.-inline-{border-color:var(--adp-border-color-inline);box-shadow:none;position:static;left:auto;right:auto;opacity:1;transform:none}.air-datepicker.-inline- .air-datepicker--pointer{display:none}.air-datepicker.-is-mobile-{--adp-font-size: var(--adp-mobile-font-size);--adp-day-cell-height: var(--adp-mobile-day-cell-height);--adp-month-cell-height: var(--adp-mobile-month-cell-height);--adp-year-cell-height: var(--adp-mobile-year-cell-height);--adp-nav-height: var(--adp-mobile-nav-height);--adp-nav-action-size: var(--adp-mobile-nav-height);position:fixed;width:var(--adp-mobile-width);border:none}.air-datepicker.-is-mobile- *{-webkit-tap-highlight-color:rgba(0,0,0,0)}.air-datepicker.-is-mobile- .air-datepicker--pointer{display:none}.air-datepicker.-is-mobile-:not(.-custom-position-){transform:translate(-50%, calc(-50% + var(--adp-transition-offset)))}.air-datepicker.-is-mobile-.-active-:not(.-custom-position-){transform:translate(-50%, -50%)}.air-datepicker.-custom-position-{transition:none}.air-datepicker-global-container{position:absolute;left:0;top:0}.air-datepicker--pointer{--pointer-half-size: calc(var(--adp-pointer-size) / 2);position:absolute;width:var(--adp-pointer-size);height:var(--adp-pointer-size);z-index:-1}.air-datepicker--pointer:after{content:"";position:absolute;background:#fff;border-top:1px solid var(--adp-border-color-inline);border-right:1px solid var(--adp-border-color-inline);border-top-right-radius:var(--adp-poiner-border-radius);width:var(--adp-pointer-size);height:var(--adp-pointer-size);box-sizing:border-box}.-top-left- .air-datepicker--pointer,.-top-center- .air-datepicker--pointer,.-top-right- .air-datepicker--pointer,[data-popper-placement^=top] .air-datepicker--pointer{top:calc(100% - var(--pointer-half-size) + 1px)}.-top-left- .air-datepicker--pointer:after,.-top-center- .air-datepicker--pointer:after,.-top-right- .air-datepicker--pointer:after,[data-popper-placement^=top] .air-datepicker--pointer:after{transform:rotate(135deg)}.-right-top- .air-datepicker--pointer,.-right-center- .air-datepicker--pointer,.-right-bottom- .air-datepicker--pointer,[data-popper-placement^=right] .air-datepicker--pointer{right:calc(100% - var(--pointer-half-size) + 1px)}.-right-top- .air-datepicker--pointer:after,.-right-center- .air-datepicker--pointer:after,.-right-bottom- .air-datepicker--pointer:after,[data-popper-placement^=right] .air-datepicker--pointer:after{transform:rotate(225deg)}.-bottom-left- .air-datepicker--pointer,.-bottom-center- .air-datepicker--pointer,.-bottom-right- .air-datepicker--pointer,[data-popper-placement^=bottom] .air-datepicker--pointer{bottom:calc(100% - var(--pointer-half-size) + 1px)}.-bottom-left- .air-datepicker--pointer:after,.-bottom-center- .air-datepicker--pointer:after,.-bottom-right- .air-datepicker--pointer:after,[data-popper-placement^=bottom] .air-datepicker--pointer:after{transform:rotate(315deg)}.-left-top- .air-datepicker--pointer,.-left-center- .air-datepicker--pointer,.-left-bottom- .air-datepicker--pointer,[data-popper-placement^=left] .air-datepicker--pointer{left:calc(100% - var(--pointer-half-size) + 1px)}.-left-top- .air-datepicker--pointer:after,.-left-center- .air-datepicker--pointer:after,.-left-bottom- .air-datepicker--pointer:after,[data-popper-placement^=left] .air-datepicker--pointer:after{transform:rotate(45deg)}.-top-left- .air-datepicker--pointer,.-bottom-left- .air-datepicker--pointer{left:var(--adp-pointer-offset)}.-top-right- .air-datepicker--pointer,.-bottom-right- .air-datepicker--pointer{right:var(--adp-pointer-offset)}.-top-center- .air-datepicker--pointer,.-bottom-center- .air-datepicker--pointer{left:calc(50% - var(--adp-pointer-size)/2)}.-left-top- .air-datepicker--pointer,.-right-top- .air-datepicker--pointer{top:var(--adp-pointer-offset)}.-left-bottom- .air-datepicker--pointer,.-right-bottom- .air-datepicker--pointer{bottom:var(--adp-pointer-offset)}.-left-center- .air-datepicker--pointer,.-right-center- .air-datepicker--pointer{top:calc(50% - var(--adp-pointer-size)/2)}.air-datepicker--navigation{grid-area:nav}.air-datepicker--content{box-sizing:content-box;padding:var(--adp-padding);grid-area:body}.-only-timepicker- .air-datepicker--content{display:none}.air-datepicker--time{grid-area:timepicker}.air-datepicker--buttons{grid-area:buttons}.air-datepicker--buttons,.air-datepicker--time{padding:var(--adp-padding);border-top:1px solid var(--adp-border-color-inner)}.air-datepicker-overlay{position:fixed;background:var(--adp-overlay-background-color);left:0;top:0;width:0;height:0;opacity:0;transition:opacity var(--adp-overlay-transition-duration) var(--adp-overlay-transition-ease),left 0s,height 0s,width 0s;transition-delay:0s,var(--adp-overlay-transition-duration),var(--adp-overlay-transition-duration),var(--adp-overlay-transition-duration);z-index:var(--adp-overlay-z-index)}.air-datepicker-overlay.-active-{opacity:1;width:100%;height:100%;transition:opacity var(--adp-overlay-transition-duration) var(--adp-overlay-transition-ease),height 0s,width 0s}
|
13
13
|
|
14
|
-
/*! tailwindcss v4.1.
|
14
|
+
/*! tailwindcss v4.1.10 | MIT License | https://tailwindcss.com */
|
15
15
|
@layer properties;
|
16
16
|
@layer theme, base, components, utilities;
|
17
17
|
@layer theme {
|
@@ -388,7 +388,7 @@
|
|
388
388
|
background-color: var(--tt-bg);
|
389
389
|
width: max-content;
|
390
390
|
pointer-events: none;
|
391
|
-
z-index:
|
391
|
+
z-index: 2;
|
392
392
|
--tw-content: attr(data-tip);
|
393
393
|
content: var(--tw-content);
|
394
394
|
}
|
@@ -1450,11 +1450,11 @@
|
|
1450
1450
|
z-index: 1;
|
1451
1451
|
position: absolute;
|
1452
1452
|
white-space: nowrap;
|
1453
|
-
top: var(--
|
1454
|
-
bottom: var(--
|
1455
|
-
left: var(--
|
1456
|
-
right: var(--
|
1457
|
-
translate: var(--
|
1453
|
+
top: var(--indicator-t, 0);
|
1454
|
+
bottom: var(--indicator-b, auto);
|
1455
|
+
left: var(--indicator-s, auto);
|
1456
|
+
right: var(--indicator-e, 0);
|
1457
|
+
translate: var(--indicator-x, 50%) var(--indicator-y, -50%);
|
1458
1458
|
}
|
1459
1459
|
}
|
1460
1460
|
.table {
|
@@ -2108,6 +2108,7 @@
|
|
2108
2108
|
border: var(--border) solid var(--input-color, color-mix(in oklab, var(--color-base-content) 20%, #0000));
|
2109
2109
|
}
|
2110
2110
|
position: relative;
|
2111
|
+
display: inline-block;
|
2111
2112
|
flex-shrink: 0;
|
2112
2113
|
cursor: pointer;
|
2113
2114
|
appearance: none;
|
@@ -2181,6 +2182,7 @@
|
|
2181
2182
|
}
|
2182
2183
|
.radio {
|
2183
2184
|
position: relative;
|
2185
|
+
display: inline-block;
|
2184
2186
|
flex-shrink: 0;
|
2185
2187
|
cursor: pointer;
|
2186
2188
|
appearance: none;
|
@@ -3057,31 +3059,6 @@
|
|
3057
3059
|
--badge-fg: var(--color-base-content);
|
3058
3060
|
--size: calc(var(--size-selector, 0.25rem) * 6);
|
3059
3061
|
height: var(--size);
|
3060
|
-
&.badge-outline {
|
3061
|
-
--badge-fg: var(--badge-color);
|
3062
|
-
--badge-bg: #0000;
|
3063
|
-
background-image: none;
|
3064
|
-
border-color: currentColor;
|
3065
|
-
}
|
3066
|
-
&.badge-dash {
|
3067
|
-
--badge-fg: var(--badge-color);
|
3068
|
-
--badge-bg: #0000;
|
3069
|
-
background-image: none;
|
3070
|
-
border-color: currentColor;
|
3071
|
-
border-style: dashed;
|
3072
|
-
}
|
3073
|
-
&.badge-soft {
|
3074
|
-
color: var(--badge-color, var(--color-base-content));
|
3075
|
-
background-color: var(--badge-color, var(--color-base-content));
|
3076
|
-
@supports (color: color-mix(in lab, red, red)) {
|
3077
|
-
background-color: color-mix( in oklab, var(--badge-color, var(--color-base-content)) 8%, var(--color-base-100) );
|
3078
|
-
}
|
3079
|
-
border-color: var(--badge-color, var(--color-base-content));
|
3080
|
-
@supports (color: color-mix(in lab, red, red)) {
|
3081
|
-
border-color: color-mix( in oklab, var(--badge-color, var(--color-base-content)) 10%, var(--color-base-100) );
|
3082
|
-
}
|
3083
|
-
background-image: none;
|
3084
|
-
}
|
3085
3062
|
}
|
3086
3063
|
.tabs {
|
3087
3064
|
display: flex;
|
@@ -3445,6 +3422,7 @@
|
|
3445
3422
|
display: grid;
|
3446
3423
|
column-gap: calc(0.25rem * 3);
|
3447
3424
|
padding-block: calc(0.25rem * 1);
|
3425
|
+
--mask-chat: url("data:image/svg+xml,%3csvg width='13' height='13' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='M0 11.5004C0 13.0004 2 13.0004 2 13.0004H12H13V0.00036329L12.5 0C12.5 0 11.977 2.09572 11.8581 2.50033C11.6075 3.35237 10.9149 4.22374 9 5.50036C6 7.50036 0 10.0004 0 11.5004Z'/%3e%3c/svg%3e");
|
3448
3426
|
}
|
3449
3427
|
.branding-label {
|
3450
3428
|
@media (width >= 64rem) {
|
@@ -4769,9 +4747,6 @@ tr:not(:first-child) th[colspan]:not([colspan="1"]) {
|
|
4769
4747
|
:root {
|
4770
4748
|
--fx-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E");
|
4771
4749
|
}
|
4772
|
-
.chat {
|
4773
|
-
--mask-chat: url("data:image/svg+xml,%3csvg width='13' height='13' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='M0 11.5004C0 13.0004 2 13.0004 2 13.0004H12H13V0.00036329L12.5 0C12.5 0 11.977 2.09572 11.8581 2.50033C11.6075 3.35237 10.9149 4.22374 9 5.50036C6 7.50036 0 10.0004 0 11.5004Z'/%3e%3c/svg%3e");
|
4774
|
-
}
|
4775
4750
|
}
|
4776
4751
|
@layer base {
|
4777
4752
|
:root:has( .modal-open, .modal[open], .modal:target, .modal-toggle:checked, .drawer:not([class*="drawer-open"]) > .drawer-toggle:checked ) {
|
Binary file
|
Binary file
|
@@ -11,7 +11,11 @@ module Karafka
|
|
11
11
|
controller = build(Controllers::ErrorsController)
|
12
12
|
|
13
13
|
r.get Integer do |offset|
|
14
|
-
|
14
|
+
if params.current_offset != -1
|
15
|
+
r.redirect root_path('errors', params.current_offset)
|
16
|
+
else
|
17
|
+
controller.show(offset)
|
18
|
+
end
|
15
19
|
end
|
16
20
|
|
17
21
|
r.get do
|
@@ -11,14 +11,9 @@
|
|
11
11
|
<% partitions_list = partitions.join(', ') %>
|
12
12
|
<span
|
13
13
|
class="badge badge-secondary"
|
14
|
-
title="Consumer group: <%= consumer_group.id %>,
|
14
|
+
title="Consumer group: <%= consumer_group.id %>, assignments: <%= topics_assignment_label(topic_name, partitions, limit: 25) %>"
|
15
15
|
>
|
16
|
-
<%= topic_name
|
17
|
-
<% if partitions.size > 10 %>
|
18
|
-
<%= "#{partitions.sort.first(10).join(',')}..." %>
|
19
|
-
<% else %>
|
20
|
-
<%= partitions.sort.join(',') %>
|
21
|
-
<% end %>
|
16
|
+
<%= topics_assignment_text(topic_name, partitions) %>
|
22
17
|
</span>
|
23
18
|
<% end %>
|
24
19
|
<% end %>
|
@@ -4,15 +4,10 @@
|
|
4
4
|
</a>
|
5
5
|
</li>
|
6
6
|
|
7
|
-
<% if @
|
7
|
+
<% if @offset %>
|
8
8
|
<li>
|
9
|
-
<a href="<%= root_path('errors', @offset) %>">
|
10
|
-
<%=
|
11
|
-
type = @error_message.payload[:type]
|
12
|
-
error_class = @error_message.payload[:error_class]
|
13
|
-
|
14
|
-
"#{type}: #{error_class}"
|
15
|
-
%>
|
9
|
+
<a href="<%= root_path('errors', @partition_id, @offset) %>">
|
10
|
+
<%= "Offset #{@offset}" %>
|
16
11
|
</a>
|
17
12
|
</li>
|
18
13
|
<% end %>
|
@@ -7,7 +7,12 @@
|
|
7
7
|
<td>
|
8
8
|
<span class="badge badge-secondary">
|
9
9
|
<% if error[:details].key?(:topic) %>
|
10
|
-
<%=
|
10
|
+
<%=
|
11
|
+
topic = error[:details][:topic]
|
12
|
+
partition = error[:details][:partition]
|
13
|
+
|
14
|
+
topics_partition_identifier(topic, partition)
|
15
|
+
%>
|
11
16
|
<% else %>
|
12
17
|
<%= error[:type] %>
|
13
18
|
<% end %>
|
@@ -1,39 +1,45 @@
|
|
1
|
-
<%
|
2
|
-
|
3
|
-
|
1
|
+
<% if @error_message %>
|
2
|
+
<%
|
3
|
+
type = @error_message.payload[:type]
|
4
|
+
error_class = @error_message.payload[:error_class]
|
4
5
|
|
5
|
-
|
6
|
-
%>
|
6
|
+
view_title "#{type}: #{error_class}"
|
7
|
+
%>
|
7
8
|
|
8
|
-
<div class="col-span-12 mb-3">
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
<div class="col-span-12 mb-3">
|
10
|
+
<h2 class="h2">
|
11
|
+
Metadata
|
12
|
+
</h2>
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
14
|
+
<div class="data-table-wrapper">
|
15
|
+
<table class="data-table">
|
16
|
+
<tbody>
|
17
|
+
<% @error_message.payload.each do |k, v| %>
|
18
|
+
<% next if k == :backtrace %>
|
19
|
+
<%==
|
20
|
+
partial(
|
21
|
+
'errors/detail',
|
22
|
+
locals: {
|
23
|
+
k: k,
|
24
|
+
v: v
|
25
|
+
}
|
26
|
+
)
|
27
|
+
%>
|
28
|
+
<% end %>
|
29
|
+
</tbody>
|
30
|
+
</table>
|
31
|
+
</div>
|
30
32
|
</div>
|
31
|
-
</div>
|
32
33
|
|
33
|
-
<div class="col-span-12 mb-
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
<div class="col-span-12 mb-6">
|
35
|
+
<h2 class="h2">
|
36
|
+
Backtrace
|
37
|
+
</h2>
|
37
38
|
|
38
|
-
|
39
|
-
</div>
|
39
|
+
<pre class="mb-5 p-0 border border-gray-300 text-sm"><code class="wrapped json p-0 m-0"><%= @error_message.payload[:backtrace] %></code></pre>
|
40
|
+
</div>
|
41
|
+
<% else %>
|
42
|
+
<% view_title "Offset #{@offset}" %>
|
43
|
+
|
44
|
+
<%== partial 'shared/compacted_message_info' %>
|
45
|
+
<% end %>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<div class="col-span-12 mb-6">
|
2
|
+
<% alert_box_info('Empty Offset - No Data') do %>
|
3
|
+
<p>
|
4
|
+
This offset does not contain any consumable message data. This is normal Kafka behavior that occurs when:
|
5
|
+
</p>
|
6
|
+
<ul class="list-disc ml-6 mt-2">
|
7
|
+
<li>The message has been removed through log compaction</li>
|
8
|
+
<li>This offset contains internal Kafka system records (transaction markers, control records)</li>
|
9
|
+
<li>The message is a tombstone record (deletion marker with null value)</li>
|
10
|
+
<li>The topic uses cleanup policies that have processed this offset</li>
|
11
|
+
</ul>
|
12
|
+
<p class="mt-2">
|
13
|
+
Try navigating to nearby offsets or verify the topic's log compaction settings if you expected message data here.
|
14
|
+
</p>
|
15
|
+
<% end %>
|
16
|
+
</div>
|