pg_eventstore 1.9.0 → 1.10.0
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 +6 -0
- data/lib/pg_eventstore/subscriptions/subscription.rb +4 -1
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore/web/application.rb +28 -7
- data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +33 -4
- data/lib/pg_eventstore/web/subscriptions/helpers.rb +31 -17
- data/lib/pg_eventstore/web/views/home/partials/events.erb +7 -1
- data/lib/pg_eventstore/web/views/subscriptions/index.erb +7 -5
- data/sig/pg_eventstore/subscriptions/subscription.rbs +2 -0
- data/sig/pg_eventstore/web/subscriptions/helpers.rbs +2 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3ff0f3d0cf7c80ebe381ca1da32b921766c10055832f05b94d15e2f25b73d55
|
4
|
+
data.tar.gz: a00e2ef1bc9435b2e5b7732e0dd5ee85a3a3ed6e457c5d106f654f035ff7c3e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23c6bd66a79199cf49411e73635d4829c5bef802a97c3e37f3d46513a1c7b8163163fa665b99d47bd870829ba2059650c4d8b9798c95991c37834e478b352aa4
|
7
|
+
data.tar.gz: af188405be2d0a13c8694840dc6dcb5eef435ada27979a2f570be1ccacf33c465930d4bb9237ad6d7c78c7bde2edc5d0cfa61064dd7dc8c2c0c6a11856f39980
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.10.0]
|
4
|
+
- Admin UI: Adjust `SubscriptionSet` "Stop"/"Delete" buttons appearance. Now if `SubscriptionsSet` is not alive anymore(the related process is dead or does not exist anymore) - "Delete" button is shown. If `SubscriptionSet` is alive - "Stop" button is shown
|
5
|
+
- Admin IU: fixed several potential XSS vulnerabilities
|
6
|
+
- Admin IU: Add "Copy to clipboard" button near stream id that copies ruby stream definition
|
7
|
+
- Admin UI: allow deletion of streams with empty attribute values
|
8
|
+
|
3
9
|
## [1.9.0]
|
4
10
|
|
5
11
|
- Implement an ability to delete a stream
|
@@ -6,6 +6,9 @@ module PgEventstore
|
|
6
6
|
include Extensions::UsingConnectionExtension
|
7
7
|
include Extensions::OptionsExtension
|
8
8
|
|
9
|
+
# @return [Time]
|
10
|
+
DEFAULT_TIMESTAMP = Time.at(0).utc.freeze
|
11
|
+
|
9
12
|
# @!attribute id
|
10
13
|
# @return [Integer, nil]
|
11
14
|
attribute(:id)
|
@@ -164,7 +167,7 @@ module PgEventstore
|
|
164
167
|
last_restarted_at: nil,
|
165
168
|
max_restarts_number: max_restarts_number,
|
166
169
|
chunk_query_interval: chunk_query_interval,
|
167
|
-
last_chunk_fed_at:
|
170
|
+
last_chunk_fed_at: DEFAULT_TIMESTAMP,
|
168
171
|
last_chunk_greatest_position: nil,
|
169
172
|
last_error: nil,
|
170
173
|
last_error_occurred_at: nil,
|
@@ -282,14 +282,35 @@ module PgEventstore
|
|
282
282
|
redirect(redirect_back_url(fallback_url: '/'))
|
283
283
|
end
|
284
284
|
|
285
|
-
post '/delete_stream
|
286
|
-
attrs =
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
message: "Stream #{stream.to_hash} has been successfully deleted.",
|
291
|
-
kind: 'success'
|
285
|
+
post '/delete_stream' do
|
286
|
+
attrs = {
|
287
|
+
context: params[:context]&.to_s,
|
288
|
+
stream_name: params[:stream_name]&.to_s,
|
289
|
+
stream_id: params[:stream_id]&.to_s,
|
292
290
|
}
|
291
|
+
|
292
|
+
err_message = ->(attrs) {
|
293
|
+
self.flash_message = {
|
294
|
+
message: "Could not delete #{attrs}. It is not valid stream for deletion.",
|
295
|
+
kind: 'error'
|
296
|
+
}
|
297
|
+
}
|
298
|
+
|
299
|
+
if attrs.values.none?(&:nil?)
|
300
|
+
stream = PgEventstore::Stream.new(**attrs)
|
301
|
+
if stream.system?
|
302
|
+
err_message.call(stream.to_hash)
|
303
|
+
else
|
304
|
+
PgEventstore.maintenance(current_config).delete_stream(stream)
|
305
|
+
self.flash_message = {
|
306
|
+
message: "Stream #{stream.to_hash} has been successfully deleted.",
|
307
|
+
kind: 'success'
|
308
|
+
}
|
309
|
+
end
|
310
|
+
else
|
311
|
+
err_message.call(attrs)
|
312
|
+
end
|
313
|
+
|
293
314
|
redirect(redirect_back_url(fallback_url: '/'))
|
294
315
|
end
|
295
316
|
end
|
@@ -196,14 +196,14 @@ $(function(){
|
|
196
196
|
let $confirmationModal = $('#confirmation-modal');
|
197
197
|
|
198
198
|
$confirmationModal.on('hide.bs.modal', function(){
|
199
|
-
$(this).find('.modal-title').
|
200
|
-
$(this).find('.modal-body').
|
199
|
+
$(this).find('.modal-title').text('');
|
200
|
+
$(this).find('.modal-body').text('');
|
201
201
|
$(this).find('.confirm').off();
|
202
202
|
});
|
203
203
|
let showConfirmation = function(el, callback){
|
204
204
|
let $el = $(el);
|
205
|
-
$confirmationModal.find('.modal-body').
|
206
|
-
$confirmationModal.find('.modal-title').
|
205
|
+
$confirmationModal.find('.modal-body').text($el.data('confirm'));
|
206
|
+
$confirmationModal.find('.modal-title').text($el.data('confirm-title'));
|
207
207
|
$confirmationModal.modal('show');
|
208
208
|
$confirmationModal.one('click', '.confirm', callback);
|
209
209
|
}
|
@@ -323,3 +323,32 @@ $(function () {
|
|
323
323
|
$flashMessage.addClass(alertClass).removeClass('d-none');
|
324
324
|
Cookies.remove(window.flashMessageCookie);
|
325
325
|
});
|
326
|
+
|
327
|
+
// Copy to clipboard with a tooltip implementation
|
328
|
+
$(function () {
|
329
|
+
"use strict";
|
330
|
+
|
331
|
+
let $selector = $(".copy-to-clipboard");
|
332
|
+
|
333
|
+
$selector.tooltip({
|
334
|
+
container: "body",
|
335
|
+
title: function(){
|
336
|
+
let $this = $(this);
|
337
|
+
|
338
|
+
return $this.data("temp-title") || $this.data("title");
|
339
|
+
}
|
340
|
+
});
|
341
|
+
|
342
|
+
$selector.click(function () {
|
343
|
+
let $this = $(this);
|
344
|
+
|
345
|
+
navigator.clipboard.writeText($this.data("clipboard-content"));
|
346
|
+
|
347
|
+
$this.data("temp-title", "Copied!");
|
348
|
+
$this.data("bs.tooltip").hide();
|
349
|
+
$this.one("shown.bs.tooltip", function(){
|
350
|
+
$this.data("temp-title", null);
|
351
|
+
});
|
352
|
+
$this.data("bs.tooltip").show();
|
353
|
+
});
|
354
|
+
});
|
@@ -79,28 +79,41 @@ module PgEventstore
|
|
79
79
|
# @param updated_at [Time]
|
80
80
|
# @return [String] html status
|
81
81
|
def colored_state(state, interval, updated_at)
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
<<~HTML
|
89
|
-
<span class="text-warning text-nowrap">
|
90
|
-
#{state}
|
91
|
-
<i class="fa fa-question-circle" data-toggle="tooltip" title="#{title}"></i>
|
92
|
-
</span>
|
93
|
-
HTML
|
82
|
+
text_class =
|
83
|
+
case state
|
84
|
+
when RunnerState::STATES[:running]
|
85
|
+
alive?(interval, updated_at) ? 'text-success' : 'text-warning'
|
86
|
+
when RunnerState::STATES[:dead]
|
87
|
+
'text-danger'
|
94
88
|
else
|
95
|
-
|
89
|
+
'text-info'
|
96
90
|
end
|
97
|
-
|
98
|
-
|
91
|
+
|
92
|
+
if alive?(interval, updated_at)
|
93
|
+
<<~HTML
|
94
|
+
<span class="#{text_class}">#{state}</span>
|
95
|
+
HTML
|
99
96
|
else
|
100
|
-
|
97
|
+
title = <<~TEXT
|
98
|
+
Something is wrong. Last update was more than #{interval} seconds ago(#{updated_at}).
|
99
|
+
TEXT
|
100
|
+
<<~HTML
|
101
|
+
<span class="#{text_class} text-nowrap">
|
102
|
+
#{state}
|
103
|
+
<i class="fa fa-question-circle" data-toggle="tooltip" title="#{title}"></i>
|
104
|
+
</span>
|
105
|
+
HTML
|
101
106
|
end
|
102
107
|
end
|
103
108
|
|
109
|
+
# @param interval [Integer]
|
110
|
+
# @param last_updated_at [Time]
|
111
|
+
# @return [Boolean]
|
112
|
+
def alive?(interval, last_updated_at)
|
113
|
+
# -1 is added as a margin to prevent false-positive result
|
114
|
+
last_updated_at > Time.now.utc - interval - 1
|
115
|
+
end
|
116
|
+
|
104
117
|
# @param ids [Array<Integer>]
|
105
118
|
# @return [String]
|
106
119
|
def delete_all_subscriptions_url(ids)
|
@@ -117,7 +130,8 @@ module PgEventstore
|
|
117
130
|
# @param stream_attrs [Hash]
|
118
131
|
# @return [String]
|
119
132
|
def delete_stream_url(stream_attrs)
|
120
|
-
|
133
|
+
encoded_params = Rack::Utils.build_nested_query(stream_attrs)
|
134
|
+
url("/delete_stream?#{encoded_params}")
|
121
135
|
end
|
122
136
|
end
|
123
137
|
end
|
@@ -4,7 +4,13 @@
|
|
4
4
|
<td><%= event.stream_revision %></td>
|
5
5
|
<td><%= h event.stream.context %></td>
|
6
6
|
<td><%= h event.stream.stream_name %></td>
|
7
|
-
<td
|
7
|
+
<td>
|
8
|
+
<a href="<%= stream_path(event) %>"><%= h event.stream.stream_id %></a>
|
9
|
+
<a role="button" href="#" data-title="Copy stream definition." class="copy-to-clipboard"
|
10
|
+
data-clipboard-content="<%= h "PgEventstore::Stream.new(context: #{event.stream.context.inspect}, stream_name: #{event.stream.stream_name.inspect}, stream_id: #{event.stream.stream_id.inspect})" %>">
|
11
|
+
<i class="fa fa-clipboard"></i>
|
12
|
+
</a>
|
13
|
+
</td>
|
8
14
|
<td>
|
9
15
|
<p class="float-left"><%= h event.type %></p>
|
10
16
|
<% if event.link %>
|
@@ -85,14 +85,16 @@
|
|
85
85
|
Restore
|
86
86
|
</a>
|
87
87
|
<% end %>
|
88
|
-
<% if PgEventstore::RunnerState::STATES.values_at(:running, :dead).include?(subscriptions_set.state) %>
|
88
|
+
<% if PgEventstore::RunnerState::STATES.values_at(:running, :dead).include?(subscriptions_set.state) && alive?(PgEventstore::SubscriptionsSetLifecycle::HEARTBEAT_INTERVAL, subscriptions_set.updated_at) %>
|
89
89
|
<a class="btn btn-warning" data-confirm="You are about to stop SubscriptionsSet#<%= subscriptions_set.id %>. This will also delete it and will result in stopping all related Subscriptions. If you used pg_eventstore CLI to start subscriptions - the related process will also be stopped. Continue?" data-confirm-title="Stop SubscriptionsSet" data-method="post" href="<%= subscriptions_set_cmd_url(subscriptions_set.id, subscriptions_set_cmd('Stop')) %>" data-toggle="tooltip" title="This action will delete Subscriptions Set and release all related Subscriptions.">
|
90
90
|
Stop
|
91
91
|
</a>
|
92
92
|
<% end %>
|
93
|
-
|
94
|
-
Delete
|
95
|
-
|
93
|
+
<% unless alive?(PgEventstore::SubscriptionsSetLifecycle::HEARTBEAT_INTERVAL, subscriptions_set.updated_at) %>
|
94
|
+
<a class="btn btn-danger" data-confirm="You are about to delete SubscriptionsSet#<%= subscriptions_set.id %>. Continue?" data-confirm-title="Delete SubscriptionsSet" data-method="post" href="<%= url("/delete_subscriptions_set/#{subscriptions_set.id}") %>" data-toggle="tooltip" title="Use this action only on stuck Subscriptions Set - to clean it up.">
|
95
|
+
Delete
|
96
|
+
</a>
|
97
|
+
<% end %>
|
96
98
|
</td>
|
97
99
|
</tr>
|
98
100
|
<% if subscriptions_set.last_error %>
|
@@ -160,7 +162,7 @@
|
|
160
162
|
</td>
|
161
163
|
<td><%= subscription.current_position %></td>
|
162
164
|
<td><%= subscription.chunk_query_interval %>s</td>
|
163
|
-
<td><%= subscription.last_chunk_fed_at %></td>
|
165
|
+
<td><%= subscription.last_chunk_fed_at if subscription.last_chunk_fed_at > PgEventstore::Subscription::DEFAULT_TIMESTAMP %></td>
|
164
166
|
<td><%= colored_state(subscription.state, PgEventstore::SubscriptionsLifecycle::HEARTBEAT_INTERVAL, subscription.updated_at) %></td>
|
165
167
|
<td>
|
166
168
|
<% if subscription.average_event_processing_time %>
|
@@ -2,6 +2,8 @@ module PgEventstore
|
|
2
2
|
module Web
|
3
3
|
module Subscriptions
|
4
4
|
module Helpers
|
5
|
+
def alive?: (Integer interval, Time last_updated_at)-> bool
|
6
|
+
|
5
7
|
def delete_event_url: (Integer global_position)-> String
|
6
8
|
|
7
9
|
def delete_stream_url: (({ context: String, stream_name: String, stream_id: String }) stream_attrs)-> String
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg_eventstore
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Dzyzenko
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-04-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pg
|