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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c0936a5580f6340a43f65d5b1b3362855b30e78fb25bdd498bf21c759458fcb
4
- data.tar.gz: d18a05c7c6917e66c83de05013afcf6b9f25aaf7027e32fb1345553f5398a120
3
+ metadata.gz: c3ff0f3d0cf7c80ebe381ca1da32b921766c10055832f05b94d15e2f25b73d55
4
+ data.tar.gz: a00e2ef1bc9435b2e5b7732e0dd5ee85a3a3ed6e457c5d106f654f035ff7c3e3
5
5
  SHA512:
6
- metadata.gz: b11fcbc08f2853a69c288dcd08498a5e40277cdeaf32ccba71ade48eac03cf3fd0c157abe0058daedb27aca945406afa8f3078f149e045ff63d89a47ca12da36
7
- data.tar.gz: c021e192cfd8f61364235fa7ccdd2d97a37c39fe304f9167fe7cda508acd7c65b84bc2d168687a1cec185cf07870183b70fc849508a434ea13935398687cbea4
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: Time.at(0).utc,
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,
@@ -2,5 +2,5 @@
2
2
 
3
3
  module PgEventstore
4
4
  # @return [String]
5
- VERSION = "1.9.0"
5
+ VERSION = "1.10.0"
6
6
  end
@@ -282,14 +282,35 @@ module PgEventstore
282
282
  redirect(redirect_back_url(fallback_url: '/'))
283
283
  end
284
284
 
285
- post '/delete_stream/:context/:stream_name/:stream_id' do
286
- attrs = Hash[params.slice(:context, :stream_name, :stream_id)].transform_keys(&:to_sym)
287
- stream = PgEventstore::Stream.new(**attrs)
288
- PgEventstore.maintenance(current_config).delete_stream(stream)
289
- self.flash_message = {
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').html('');
200
- $(this).find('.modal-body').html('');
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').html($el.data('confirm'));
206
- $confirmationModal.find('.modal-title').html($el.data('confirm-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
- if state == RunnerState::STATES[:running]
83
- # -1 is added as a margin to prevent false-positive result
84
- if updated_at < Time.now.utc - interval - 1
85
- title = <<~TEXT
86
- Something is wrong. Last update was more than #{interval} seconds ago(#{updated_at}).
87
- TEXT
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
- "<span class=\"text-success\">#{state}</span>"
89
+ 'text-info'
96
90
  end
97
- elsif state == RunnerState::STATES[:dead]
98
- "<span class=\"text-danger\">#{state}</span>"
91
+
92
+ if alive?(interval, updated_at)
93
+ <<~HTML
94
+ <span class="#{text_class}">#{state}</span>
95
+ HTML
99
96
  else
100
- "<span class=\"text-info\">#{state}</span>"
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
- url("/delete_stream/#{stream_attrs[:context]}/#{stream_attrs[:stream_name]}/#{stream_attrs[:stream_id]}")
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><a href="<%= stream_path(event) %>"><%= event.stream.stream_id %></a></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
- <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.">
94
- Delete
95
- </a>
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 %>
@@ -1,5 +1,7 @@
1
1
  module PgEventstore
2
2
  class Subscription
3
+ DEFAULT_TIMESTAMP: Time
4
+
3
5
  include PgEventstore::Extensions::UsingConnectionExtension
4
6
  include PgEventstore::Extensions::OptionsExtension
5
7
 
@@ -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.9.0
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-03-14 00:00:00.000000000 Z
11
+ date: 2025-04-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg