ruby_event_store-browser 3.0.1 → 3.1.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/README.md +1 -1
- data/lib/ruby_event_store/browser/app.rb +190 -86
- data/lib/ruby_event_store/browser/extension_context.rb +22 -0
- data/lib/ruby_event_store/browser/get_events_from_stream.rb +50 -93
- data/lib/ruby_event_store/browser/get_events_from_streams.rb +98 -0
- data/lib/ruby_event_store/browser/layout.rb +55 -0
- data/lib/ruby_event_store/browser/renderer.rb +64 -0
- data/lib/ruby_event_store/browser/urls.rb +35 -39
- data/lib/ruby_event_store/browser/version.rb +1 -1
- data/lib/ruby_event_store/browser/views/_clipboard_icon.html.erb +1 -0
- data/lib/ruby_event_store/browser/views/_clock_icon.html.erb +1 -0
- data/lib/ruby_event_store/browser/views/_json_tree.html.erb +1 -0
- data/lib/ruby_event_store/browser/views/_raw_section.html.erb +9 -0
- data/lib/ruby_event_store/browser/views/_timestamp.html.erb +7 -0
- data/lib/ruby_event_store/browser/views/events/show.html.erb +85 -0
- data/lib/ruby_event_store/browser/views/layout.html.erb +72 -0
- data/lib/ruby_event_store/browser/views/not_found.html.erb +3 -0
- data/lib/ruby_event_store/browser/views/streams/show.html.erb +74 -0
- data/lib/ruby_event_store/browser/views/swimlane/_rows.html.erb +20 -0
- data/lib/ruby_event_store/browser/views/swimlane/show.html.erb +55 -0
- data/lib/ruby_event_store/browser.rb +17 -5
- data/public/ruby_event_store_browser.css +2 -1
- data/public/ruby_event_store_browser.js +287 -1
- data/public/stimulus-3.2.2.js +11 -0
- metadata +21 -11
- data/lib/ruby_event_store/browser/gem_source.rb +0 -32
- data/lib/ruby_event_store/browser/get_event.rb +0 -37
- data/lib/ruby_event_store/browser/get_stream.rb +0 -29
- data/lib/ruby_event_store/browser/json_api_event.rb +0 -66
- data/lib/ruby_event_store/browser/json_api_stream.rb +0 -34
- data/public/bootstrap.js +0 -30
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyEventStore
|
|
4
|
+
module Browser
|
|
5
|
+
class Layout
|
|
6
|
+
def initialize(stylesheets_resolver, scripts_resolver, nav_links_resolver, views_roots:)
|
|
7
|
+
@stylesheets_resolver = stylesheets_resolver
|
|
8
|
+
@scripts_resolver = scripts_resolver
|
|
9
|
+
@nav_links_resolver = nav_links_resolver
|
|
10
|
+
@views_roots = views_roots
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def with_views_root(views_root)
|
|
14
|
+
Layout.new(
|
|
15
|
+
@stylesheets_resolver,
|
|
16
|
+
@scripts_resolver,
|
|
17
|
+
@nav_links_resolver,
|
|
18
|
+
views_roots: @views_roots + [views_root],
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render(template, urls:, title:, **locals)
|
|
23
|
+
content = renderer.render(template, urls: urls, **locals)
|
|
24
|
+
[200, { "content-type" => "text/html;charset=utf-8" }, [wrap(renderer, content, urls, title)]]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def render_partial(template, urls:, **locals)
|
|
28
|
+
renderer.render(template, urls: urls, **locals)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def not_found(urls, message:)
|
|
32
|
+
content = renderer.render("not_found", message: message)
|
|
33
|
+
[404, { "content-type" => "text/html;charset=utf-8" }, [wrap(renderer, content, urls, "Not found")]]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def renderer
|
|
39
|
+
Renderer.new([*@views_roots, Renderer::VIEWS_ROOT])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def wrap(renderer, content, urls, title)
|
|
43
|
+
renderer.render(
|
|
44
|
+
"layout",
|
|
45
|
+
content: content,
|
|
46
|
+
urls: urls,
|
|
47
|
+
title: title,
|
|
48
|
+
extension_stylesheets: @stylesheets_resolver.call(urls),
|
|
49
|
+
extension_scripts: @scripts_resolver.call(urls),
|
|
50
|
+
extension_nav_links: @nav_links_resolver.call(urls),
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module RubyEventStore
|
|
7
|
+
module Browser
|
|
8
|
+
class Renderer
|
|
9
|
+
VIEWS_ROOT = File.expand_path("views", __dir__).freeze
|
|
10
|
+
|
|
11
|
+
class Context
|
|
12
|
+
include ERB::Util
|
|
13
|
+
alias_method :h, :html_escape
|
|
14
|
+
|
|
15
|
+
def initialize(renderer, locals)
|
|
16
|
+
@_renderer = renderer
|
|
17
|
+
locals.each { |k, v| define_singleton_method(k) { v } }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render(template, **locals)
|
|
21
|
+
@_renderer.render(template, **locals)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def safe_json(value)
|
|
25
|
+
if value.is_a?(Float) && value.infinite? && value.positive?
|
|
26
|
+
'"Infinity"'
|
|
27
|
+
elsif value.is_a?(Float) && value.infinite?
|
|
28
|
+
'"-Infinity"'
|
|
29
|
+
elsif value.is_a?(Float) && value.nan?
|
|
30
|
+
'"NaN"'
|
|
31
|
+
elsif value.is_a?(Float) && value == value.to_i
|
|
32
|
+
value.to_i.to_s
|
|
33
|
+
elsif value.is_a?(Hash)
|
|
34
|
+
"{#{value.map { |k, v| "#{k.to_json}:#{safe_json(v)}" }.join(",")}}"
|
|
35
|
+
elsif value.is_a?(Array)
|
|
36
|
+
"[#{value.map { |v| safe_json(v) }.join(",")}]"
|
|
37
|
+
else
|
|
38
|
+
value.to_json
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def get_binding
|
|
43
|
+
binding
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialize(views_roots = [VIEWS_ROOT])
|
|
48
|
+
@views_roots = views_roots
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def render(template, **locals)
|
|
52
|
+
context = Context.new(self, locals)
|
|
53
|
+
ERB.new(File.read(template_path(template)), trim_mode: "-").result(context.get_binding)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def template_path(template)
|
|
59
|
+
@views_roots.map { |root| File.join(root, "#{template}.html.erb") }.find { |path| File.exist?(path) } ||
|
|
60
|
+
raise(ArgumentError, "Template not found: #{template}")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -3,78 +3,74 @@
|
|
|
3
3
|
module RubyEventStore
|
|
4
4
|
module Browser
|
|
5
5
|
class Urls
|
|
6
|
-
def self.from_configuration(host, root_path
|
|
7
|
-
new(host, root_path
|
|
6
|
+
def self.from_configuration(host, root_path)
|
|
7
|
+
new(host, root_path)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def self.initial
|
|
11
|
-
new(nil, nil
|
|
11
|
+
new(nil, nil)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def with_request(request)
|
|
15
|
-
Urls.new(host || request.base_url, root_path || request.script_name
|
|
15
|
+
Urls.new(host || request.base_url, root_path || request.script_name)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
attr_reader :app_url, :
|
|
18
|
+
attr_reader :app_url, :host, :root_path
|
|
19
19
|
|
|
20
|
-
def initialize(host, root_path
|
|
20
|
+
def initialize(host, root_path)
|
|
21
21
|
@host = host
|
|
22
22
|
@root_path = root_path
|
|
23
23
|
@app_url = [host, root_path].compact.reduce(:+)
|
|
24
|
-
@api_url = api_url || ("#{app_url}/api" if app_url)
|
|
25
|
-
@gem_source = GemSource.new($LOAD_PATH)
|
|
26
24
|
end
|
|
27
25
|
|
|
28
|
-
def
|
|
29
|
-
"#{
|
|
26
|
+
def stream_url(stream_name)
|
|
27
|
+
"#{app_url}/streams/#{Rack::Utils.escape(stream_name)}"
|
|
30
28
|
end
|
|
31
29
|
|
|
32
|
-
def
|
|
33
|
-
"#{
|
|
30
|
+
def event_url(event_id)
|
|
31
|
+
"#{app_url}/events/#{event_id}"
|
|
34
32
|
end
|
|
35
33
|
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
query_string =
|
|
34
|
+
def stream_page_url(stream_name, cursor, count)
|
|
35
|
+
query =
|
|
39
36
|
URI.encode_www_form(
|
|
40
|
-
|
|
37
|
+
[["page[position]", cursor[:position]], ["page[direction]", cursor[:direction]], ["page[count]", count]],
|
|
41
38
|
)
|
|
42
|
-
|
|
43
|
-
if query_string.empty?
|
|
44
|
-
"#{api_url}/streams/#{stream_name}/relationships/events"
|
|
45
|
-
else
|
|
46
|
-
"#{api_url}/streams/#{stream_name}/relationships/events?#{query_string}"
|
|
47
|
-
end
|
|
39
|
+
"#{stream_url(stream_name)}?#{query}"
|
|
48
40
|
end
|
|
49
41
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
gem_source.from_git? ? cdn_file_url(name) : local_file_url(name)
|
|
42
|
+
def swimlane_url(stream_names, sort = nil)
|
|
43
|
+
"#{app_url}/swimlane?#{swimlane_query(stream_names, sort)}"
|
|
53
44
|
end
|
|
54
45
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
gem_source.from_git? ? cdn_file_url(name) : local_file_url(name)
|
|
46
|
+
def swimlane_more_url(stream_names, cursor, sort)
|
|
47
|
+
"#{app_url}/swimlane/more?#{swimlane_query(stream_names, sort, [["cursor", cursor]])}"
|
|
58
48
|
end
|
|
59
49
|
|
|
60
|
-
def
|
|
61
|
-
|
|
50
|
+
def app_url_for(*segments, query: nil)
|
|
51
|
+
path = segments.map { |segment| Rack::Utils.escape(segment) }.join("/")
|
|
52
|
+
query ? "#{app_url}/#{path}?#{URI.encode_www_form(query)}" : "#{app_url}/#{path}"
|
|
62
53
|
end
|
|
63
54
|
|
|
64
|
-
def
|
|
65
|
-
|
|
55
|
+
def browser_js_url
|
|
56
|
+
"#{app_url}/#{BROWSER_JS}"
|
|
66
57
|
end
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
def browser_css_url
|
|
60
|
+
"#{app_url}/#{BROWSER_CSS}"
|
|
61
|
+
end
|
|
71
62
|
|
|
72
|
-
def
|
|
73
|
-
|
|
63
|
+
def ==(other)
|
|
64
|
+
self.class.eql?(other.class) && app_url.eql?(other.app_url)
|
|
74
65
|
end
|
|
75
66
|
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def swimlane_query(stream_names, sort, extra = [])
|
|
70
|
+
pairs = stream_names.map { |name| ["streams[]", name] }
|
|
71
|
+
pairs.concat(extra)
|
|
72
|
+
pairs << ["sort", sort] if sort
|
|
73
|
+
URI.encode_www_form(pairs)
|
|
78
74
|
end
|
|
79
75
|
end
|
|
80
76
|
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg class="<%= classes %>" fill="none" height="<%= size %>" width="<%= size %>" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg class="size-3 text-gray-400" fill="none" height="24" width="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="font-[monospace] whitespace-pre">{<ul class="list-none ml-[26px] pl-0"><% hash.sort_by { |key, _| key.to_s }.each do |key, value| %><li class="relative"><span class="font-bold"><%= h(key) %></span>: <span class="<%= case value when String then "text-[green]" when Numeric then "text-[blue]" when true, false then "text-[firebrick]" when nil then "text-[gray]" else "text-gray-800" end %>"><%= h(safe_json(value)) %></span>,</li><% end %></ul>}</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<section class="space-y-4">
|
|
2
|
+
<header class="flex justify-between border-gray-400 border-b text-xs pb-2">
|
|
3
|
+
<h2 class="text-gray-500 uppercase font-bold"><%= title %></h2>
|
|
4
|
+
<button data-controller="clipboard" data-clipboard-text-value="<%= h(safe_json(hash)) %>" data-action="clipboard#copy" class="text-red-700 no-underline"><%= render("_clipboard_icon", classes: "text-gray-400 hover:text-red-700", size: 16) %></button>
|
|
5
|
+
</header>
|
|
6
|
+
<div class="overflow-auto w-full">
|
|
7
|
+
<%= render("_json_tree", hash: hash) %>
|
|
8
|
+
</div>
|
|
9
|
+
</section>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<section class="space-y-1<%= top ? " pt-3" : "" %> cursor-help" data-timezone-target="zone" title="UTC">
|
|
2
|
+
<header class="flex items-center gap-1 text-xs">
|
|
3
|
+
<%= render("_clock_icon") %>
|
|
4
|
+
<h2 class="text-gray-500 uppercase font-bold"><%= title %></h2>
|
|
5
|
+
</header>
|
|
6
|
+
<div class="overflow-auto w-full text-sm font-bold font-mono pl-4 text-gray-700 tracking-tight" data-timezone-target="time" data-iso="<%= time.utc.iso8601(6) %>"><%= time.utc.strftime("%Y-%m-%dT%H:%M:%S.%3N") %></div>
|
|
7
|
+
</section>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<div class="py-8 md:py-12 container mx-auto space-y-10">
|
|
2
|
+
<header class="flex items-start justify-between gap-4 flex-wrap md:flex-nowrap">
|
|
3
|
+
<div class="flex flex-col items-start">
|
|
4
|
+
<h1 class="font-bold text-2xl break-words min-w-0 mb-2"><%= h(event.event_type) %></h1>
|
|
5
|
+
<p class="flex gap-2 md:items-center min-w-0 text-sm flex-wrap mb-4">
|
|
6
|
+
<span class="whitespace-nowrap text-xs text-gray-500 uppercase font-bold">Event ID:</span>
|
|
7
|
+
<button data-controller="clipboard" data-clipboard-text-value="<%= event.event_id %>" data-action="clipboard#copy" class="flex items-center text-left gap-2 group font-mono text-gray-800 font-bold text-sm">
|
|
8
|
+
<%= event.event_id %>
|
|
9
|
+
<%= render("_clipboard_icon", classes: "size-4 -translate-y-0.5 opacity-0 group-hover:opacity-100", size: 24) %>
|
|
10
|
+
</button>
|
|
11
|
+
</p>
|
|
12
|
+
<% if extension_links.any? -%>
|
|
13
|
+
<div class="flex gap-2">
|
|
14
|
+
<% extension_links.each do |link| -%>
|
|
15
|
+
<a class="inline-block text-center text-sm bg-red-700 text-gray-100 border border-red-700 rounded px-2 py-1" href="<%= h(link[:url]) %>"><%= h(link[:label]) %></a>
|
|
16
|
+
<% end -%>
|
|
17
|
+
</div>
|
|
18
|
+
<% end -%>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="space-y-4">
|
|
21
|
+
<%= render("_timestamp", title: "Created at", time: event.metadata[:timestamp], top: true) %>
|
|
22
|
+
<%= render("_timestamp", title: "Valid at", time: event.metadata[:valid_at], top: false) %>
|
|
23
|
+
</div>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<div class="w-full text-left grid md:grid-cols-2 gap-8 overflow-hidden">
|
|
27
|
+
<%= render("_raw_section", title: "Raw Data", hash: event.data) %>
|
|
28
|
+
<%= render("_raw_section", title: "Raw Metadata", hash: metadata) %>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div>
|
|
32
|
+
<h2 class="font-bold text-xl mb-4">Event streams</h2>
|
|
33
|
+
<ul class="list-disc pl-4 space-y-2">
|
|
34
|
+
<% streams.each do |stream| -%>
|
|
35
|
+
<li>
|
|
36
|
+
<a class="text-red-700 no-underline" href="<%= urls.stream_url(stream) %>"><%= h(stream) %></a>
|
|
37
|
+
</li>
|
|
38
|
+
<% end -%>
|
|
39
|
+
</ul>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<% if parent_event -%>
|
|
43
|
+
<div>
|
|
44
|
+
<h2 class="font-bold text-xl mb-4">Related</h2>
|
|
45
|
+
<ul class="list-disc pl-4 space-y-2">
|
|
46
|
+
<li>Parent event: <a class="text-red-700 no-underline" href="<%= urls.event_url(parent_event.event_id) %>"><%= parent_event.event_id %></a></li>
|
|
47
|
+
</ul>
|
|
48
|
+
</div>
|
|
49
|
+
<% end -%>
|
|
50
|
+
|
|
51
|
+
<% if caused_by.any? -%>
|
|
52
|
+
<div class="space-y-4">
|
|
53
|
+
<h2 class="font-bold text-xl">Events caused by this event:</h2>
|
|
54
|
+
<table class="w-full text-left table-fixed">
|
|
55
|
+
<thead class="align-bottom leading-tight sticky top-0 bg-white/70 backdrop-blur-sm text-gray-500 uppercase text-xs">
|
|
56
|
+
<tr class="border-gray-400 border-b">
|
|
57
|
+
<th class="p-4">Event name</th>
|
|
58
|
+
<th class="py-4 pr-4 lg:w-80">Event id</th>
|
|
59
|
+
</tr>
|
|
60
|
+
</thead>
|
|
61
|
+
<tbody class="align-top">
|
|
62
|
+
<% caused_by.each do |child| -%>
|
|
63
|
+
<tr class="border-gray-50 border-b hover:bg-gray-100">
|
|
64
|
+
<td>
|
|
65
|
+
<a class="text-red-700 no-underline min-h-11 w-full flex items-center px-4" href="<%= urls.event_url(child.event_id) %>"><%= h(child.event_type) %></a>
|
|
66
|
+
</td>
|
|
67
|
+
<td class="font-mono text-sm leading-none font-medium align-middle">
|
|
68
|
+
<a class="no-underline h-full min-h-11 flex items-center" href="<%= urls.event_url(child.event_id) %>"><%= child.event_id %></a>
|
|
69
|
+
</td>
|
|
70
|
+
</tr>
|
|
71
|
+
<% end -%>
|
|
72
|
+
</tbody>
|
|
73
|
+
<tfoot>
|
|
74
|
+
<tr>
|
|
75
|
+
<td class="text-gray-500 py-4 text-center text-xs" colspan="2"><%= "The results may be truncated, check stream for full information." if caused_by.size == PAGE_SIZE %></td>
|
|
76
|
+
</tr>
|
|
77
|
+
</tfoot>
|
|
78
|
+
</table>
|
|
79
|
+
</div>
|
|
80
|
+
<% else -%>
|
|
81
|
+
<div>
|
|
82
|
+
<h2 class="font-bold text-xl">Events caused by this event: none</h2>
|
|
83
|
+
</div>
|
|
84
|
+
<% end -%>
|
|
85
|
+
</div>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<title>RubyEventStore::Browser<% if defined?(title) && title %> - <%= h(title) %><% end %></title>
|
|
5
|
+
<link type="text/css" rel="stylesheet" href="<%= urls.browser_css_url %>">
|
|
6
|
+
<% extension_stylesheets.each do |href| -%>
|
|
7
|
+
<link type="text/css" rel="stylesheet" href="<%= href %>">
|
|
8
|
+
<% end -%>
|
|
9
|
+
<meta name="robots" content="noindex, nofollow">
|
|
10
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
|
11
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
|
12
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
|
13
|
+
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
|
14
|
+
<meta name="msapplication-TileColor" content="#da532c">
|
|
15
|
+
<meta name="theme-color" content="#ffffff">
|
|
16
|
+
</head>
|
|
17
|
+
<body data-controller="swimlane" data-swimlane-base-url-value="<%= urls.app_url %>">
|
|
18
|
+
<div class="flex flex-col w-full min-h-screen font-sans antialiased leading-relaxed text-gray-800 bg-gray-100" data-controller="search timezone" data-search-base-value="<%= urls.app_url %>" data-action="keydown.meta+k@window->search#open keydown.ctrl+k@window->search#open">
|
|
19
|
+
<header>
|
|
20
|
+
<nav class="sticky flex justify-between h-16 px-8 bg-red-700">
|
|
21
|
+
<div class="flex items-center gap-6">
|
|
22
|
+
<a href="<%= urls.app_url %>" class="font-semibold text-gray-100">Ruby Event Store</a>
|
|
23
|
+
<% extension_nav_links.each do |link| -%>
|
|
24
|
+
<a href="<%= h(link[:url]) %>" class="text-sm text-red-100 hover:text-white"><%= h(link[:label]) %></a>
|
|
25
|
+
<% end -%>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="flex items-center gap-2">
|
|
28
|
+
<button class="flex items-center gap-2 px-3 text-sm text-red-100 bg-red-800 rounded outline-none hover:bg-red-900 h-9" data-action="search#open">
|
|
29
|
+
<svg class="size-4" fill="none" height="24" width="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>Go to stream…<span class="text-xs">⌘K</span>
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
</nav>
|
|
33
|
+
</header>
|
|
34
|
+
<main class="flex w-full px-4 bg-white grow">
|
|
35
|
+
<%= content %>
|
|
36
|
+
<dialog data-search-target="dialog" class="open:flex flex-col items-center justify-center fixed inset-0 size-full max-w-full max-h-full bg-transparent backdrop:bg-black/50">
|
|
37
|
+
<button class="fixed inset-0 -z-10 cursor-default" data-action="search#close" tabindex="-1" aria-label="Close search"></button>
|
|
38
|
+
<form data-action="search#go" class="w-full max-w-md p-4 bg-white rounded-lg shadow-xl">
|
|
39
|
+
<div class="relative">
|
|
40
|
+
<svg class="size-4 text-gray-400 absolute pointer-events-none top-3.5 left-2" fill="none" height="24" width="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
|
41
|
+
<input data-search-target="input" class="w-full py-2 pl-8 pr-12 text-gray-800 rounded outline-none appearance-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50" placeholder="Go to stream…" autofocus>
|
|
42
|
+
<span class="absolute top-0 h-full flex items-center right-3 text-[.5rem] pointer-events-none">
|
|
43
|
+
<span class="block p-1 font-bold text-gray-500 border border-gray-300 rounded bg-gray-50">ESC</span>
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
</form>
|
|
47
|
+
</dialog>
|
|
48
|
+
</main>
|
|
49
|
+
<footer class="flex justify-between px-8 py-4 pb-20 border-t border-gray-400">
|
|
50
|
+
<div class="text-sm text-gray-500">RubyEventStore v<%= RubyEventStore::VERSION %><span class="inline-block ml-4 font-bold text-gray-400">•</span><a href="https://railseventstore.org/docs/getting-started/install" class="ml-4">Documentation</a><span class="inline-block ml-4 font-bold text-gray-400">•</span><a href="https://railseventstore.org/support/" class="ml-4">Support</a></div>
|
|
51
|
+
<div class="flex gap-2 text-sm text-gray-500 item-center">Display times in timezone:<select data-timezone-target="select" data-action="change->timezone#change">
|
|
52
|
+
<option value="UTC">UTC</option>
|
|
53
|
+
</select>
|
|
54
|
+
</div>
|
|
55
|
+
</footer>
|
|
56
|
+
</div>
|
|
57
|
+
<script type="module" src="<%= urls.browser_js_url %>"></script>
|
|
58
|
+
<% extension_scripts.each do |src| -%>
|
|
59
|
+
<script type="module" src="<%= src %>"></script>
|
|
60
|
+
<% end -%>
|
|
61
|
+
<div data-swimlane-target="drawer" class="fixed z-10 hidden has-[li]:flex flex-col items-end justify-end gap-4 bottom-8 right-8">
|
|
62
|
+
<div data-swimlane-target="drawer-content" class="hidden [[data-swimlane-drawer-open]_&]:flex flex-col items-stretch gap-3 p-4 bg-white rounded-md shadow-md w-72 max-w-[90vw]">
|
|
63
|
+
<ul data-swimlane-target="drawerList" class="flex flex-col divide-y divide-gray-100"></ul>
|
|
64
|
+
<button data-action="swimlane#go" title="Go to swimlane view" class="inline-flex items-center justify-center gap-2 px-3 py-1.5 text-sm text-gray-100 bg-red-700 rounded hover:bg-red-800">
|
|
65
|
+
<span>Go to swimlane view</span>
|
|
66
|
+
<svg class="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
<button class="flex items-center justify-center text-xl text-white bg-blue-800 rounded-full size-10" data-action="swimlane#toggleDrawer"><span class="sr-only">Toggle swimlane drawer</span>≈</button>
|
|
70
|
+
</div>
|
|
71
|
+
</body>
|
|
72
|
+
</html>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<div class="container py-8 mx-auto">
|
|
2
|
+
<div class="flex justify-between">
|
|
3
|
+
<h1 class="text-2xl font-bold">Events in <%= h(stream_name) %> </h1>
|
|
4
|
+
<div>
|
|
5
|
+
<ul class="flex">
|
|
6
|
+
<% [["first", pagination[:first]], ["previous", pagination[:prev]], ["next", pagination[:next]], ["last", pagination[:last]]].each do |label, url| -%>
|
|
7
|
+
<li>
|
|
8
|
+
<% if url -%>
|
|
9
|
+
<a class="px-2 py-1 mr-1 text-sm text-center text-red-700 border border-red-700 rounded" href="<%= url %>"><%= label %></a>
|
|
10
|
+
<% else -%>
|
|
11
|
+
<a class="px-2 py-1 mr-1 text-sm text-center border rounded cursor-not-allowed text-red-700/50 border-red-700/50"><%= label %></a>
|
|
12
|
+
<% end -%>
|
|
13
|
+
</li>
|
|
14
|
+
<% end -%>
|
|
15
|
+
</ul>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
<% unless stream_name == SERIALIZED_GLOBAL_STREAM_NAME -%>
|
|
19
|
+
<button class="inline-flex items-center gap-1 px-2 py-1 mt-3 text-sm text-center text-gray-100 bg-blue-800 border border-blue-800 rounded hover:bg-blue-900" data-stream-name="<%= h(stream_name) %>" data-action="swimlane#add">
|
|
20
|
+
<svg class="pointer-events-none size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
21
|
+
Add to swimlane
|
|
22
|
+
</button>
|
|
23
|
+
<% end -%>
|
|
24
|
+
|
|
25
|
+
<% links = [*extension_links].compact -%>
|
|
26
|
+
<% if links.any? -%>
|
|
27
|
+
<div class="flex gap-2 pt-3">
|
|
28
|
+
<% links.each do |link| -%>
|
|
29
|
+
<a class="inline-block px-2 py-1 text-sm text-center text-gray-100 bg-red-700 border border-red-700 rounded" href="<%= h(link[:url]) %>"><%= h(link[:label]) %></a>
|
|
30
|
+
<% end -%>
|
|
31
|
+
</div>
|
|
32
|
+
<% end -%>
|
|
33
|
+
<div>
|
|
34
|
+
<div class="w-full overflow-x-scroll sm:overflow-visible">
|
|
35
|
+
<table class="w-full my-10 text-left lg:table-fixed">
|
|
36
|
+
<thead class="sticky top-0 text-xs leading-tight text-gray-500 uppercase align-bottom bg-white/70 backdrop-blur-sm">
|
|
37
|
+
<tr class="border-b border-gray-400">
|
|
38
|
+
<th class="p-4">Event name</th>
|
|
39
|
+
<th class="py-4 pr-4 lg:w-80">Event id</th>
|
|
40
|
+
<th class="py-4 pr-4 text-right lg:w-60"><span class="cursor-help" data-timezone-target="zone" title="UTC">Created at</span></th>
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody class="align-top">
|
|
44
|
+
<% events.each do |event| -%>
|
|
45
|
+
<% href = urls.event_url(event.event_id) -%>
|
|
46
|
+
<tr class="border-b border-gray-50 hover:bg-gray-100">
|
|
47
|
+
<td>
|
|
48
|
+
<a class="flex items-center w-full px-4 text-red-700 no-underline min-h-11" href="<%= href %>"><span class="min-w-0 break-words"><%= h(event.event_type) %></span></a>
|
|
49
|
+
</td>
|
|
50
|
+
<td class="font-mono text-sm font-medium leading-none align-middle">
|
|
51
|
+
<a class="flex items-center h-full no-underline min-h-11" href="<%= href %>"><%= event.event_id %></a>
|
|
52
|
+
</td>
|
|
53
|
+
<td class="font-mono text-sm font-medium leading-none">
|
|
54
|
+
<a class="flex items-center justify-end px-4 no-underline min-h-11" href="<%= href %>"><span data-timezone-target="time" data-iso="<%= event.metadata[:timestamp].utc.iso8601(6) %>" title="UTC"><%= event.metadata[:timestamp].utc.strftime("%Y-%m-%dT%H:%M:%S.%3N") %></span></a>
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
<% end -%>
|
|
58
|
+
</tbody>
|
|
59
|
+
</table>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div>
|
|
63
|
+
<% if related_streams && related_streams.any? -%>
|
|
64
|
+
<div>
|
|
65
|
+
<h2 class="text-xl font-bold">Related streams:</h2>
|
|
66
|
+
<ul class="pl-8 list-disc">
|
|
67
|
+
<% related_streams.each do |stream| -%>
|
|
68
|
+
<li><a class="text-red-700 no-underline" href="<%= urls.stream_url(stream) %>"><%= h(stream) %></a></li>
|
|
69
|
+
<% end -%>
|
|
70
|
+
</ul>
|
|
71
|
+
</div>
|
|
72
|
+
<% end -%>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<% time_field = sort == "as_of" ? :valid_at : :timestamp -%>
|
|
2
|
+
<% events.each_with_index do |(names, event), index| -%>
|
|
3
|
+
<% next_event = events[index + 1]&.last -%>
|
|
4
|
+
<% same_group = next_event && next_event.metadata[time_field].utc.iso8601(3) == event.metadata[time_field].utc.iso8601(3) -%>
|
|
5
|
+
<% cell = "align-middle px-2 py-1#{" border-b border-gray-300" unless same_group}" -%>
|
|
6
|
+
<tr>
|
|
7
|
+
<td class="<%= cell %> font-mono text-xs text-gray-500 whitespace-nowrap">
|
|
8
|
+
<% unless same_group -%>
|
|
9
|
+
<span data-swimlane-view-target="time" data-iso="<%= event.metadata[time_field].utc.iso8601(6) %>" title="UTC"><%= event.metadata[time_field].utc.strftime("%Y-%m-%dT%H:%M:%S.%3N") %></span>
|
|
10
|
+
<% end -%>
|
|
11
|
+
</td>
|
|
12
|
+
<% stream_names.each_with_index do |stream_name, column| -%>
|
|
13
|
+
<td class="<%= cell %><%= " border-l border-gray-200" if column.positive? %><%= " hover:bg-gray-100" if names.include?(stream_name) %>">
|
|
14
|
+
<% if names.include?(stream_name) -%>
|
|
15
|
+
<a href="<%= urls.event_url(event.event_id) %>" class="no-underline text-red-700 text-sm break-words"><%= h(event.event_type) %></a>
|
|
16
|
+
<% end -%>
|
|
17
|
+
</td>
|
|
18
|
+
<% end -%>
|
|
19
|
+
</tr>
|
|
20
|
+
<% end -%>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<div class="w-full shrink-0" data-controller="swimlane-view swimlane-add" data-swimlane-view-more-url-value="<%= more_url %>" data-swimlane-add-base-url-value="<%= urls.swimlane_url(stream_names, sort) %>">
|
|
2
|
+
<div class="container py-8 mx-auto">
|
|
3
|
+
<div class="flex items-center justify-between">
|
|
4
|
+
<h1 class="text-2xl font-bold">Comparing <%= h(stream_names.join(", ")) %></h1>
|
|
5
|
+
<button class="px-2 py-1 text-sm text-center text-red-700 border border-red-700 rounded" data-action="swimlane-add#open">Add stream</button>
|
|
6
|
+
</div>
|
|
7
|
+
<% missing_stream_names.each do |stream_name| -%>
|
|
8
|
+
<p class="px-4 py-3 mt-4 text-sm text-yellow-800 border border-yellow-300 rounded bg-yellow-50">
|
|
9
|
+
Stream <span class="font-bold"><%= h(stream_name) %></span> does not exist.
|
|
10
|
+
</p>
|
|
11
|
+
<% end -%>
|
|
12
|
+
<% if stream_names.any? -%>
|
|
13
|
+
<div class="flex items-center gap-2 pt-3 text-sm">
|
|
14
|
+
<span class="text-gray-500">Sort by</span>
|
|
15
|
+
<% if sort == "as_of" -%>
|
|
16
|
+
<a class="text-gray-500 no-underline hover:text-red-700" href="<%= urls.swimlane_url(stream_names) %>">created at</a>
|
|
17
|
+
<span class="font-bold text-red-700">valid at</span>
|
|
18
|
+
<% else -%>
|
|
19
|
+
<span class="font-bold text-red-700">created at</span>
|
|
20
|
+
<a class="text-gray-500 no-underline hover:text-red-700" href="<%= urls.swimlane_url(stream_names, "as_of") %>">valid at</a>
|
|
21
|
+
<% end -%>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="my-10 overflow-x-auto">
|
|
24
|
+
<table class="text-left">
|
|
25
|
+
<thead class="text-xs leading-tight text-gray-500 uppercase align-bottom">
|
|
26
|
+
<tr>
|
|
27
|
+
<th class="px-2 pb-2 text-base font-bold text-gray-800 normal-case border-b border-gray-400 whitespace-nowrap">Time</th>
|
|
28
|
+
<% stream_names.each_with_index do |stream_name, column| -%>
|
|
29
|
+
<th class="font-bold border-b border-gray-400 pb-2 px-2 normal-case text-base text-gray-800<%= " border-l" if column.positive? %>">
|
|
30
|
+
<div class="flex items-center justify-between gap-2">
|
|
31
|
+
<span class="break-words"><%= h(stream_name) %></span>
|
|
32
|
+
<% if stream_names.size > 1 -%>
|
|
33
|
+
<a class="text-xl leading-none text-gray-400 no-underline hover:text-red-700" href="<%= urls.swimlane_url(stream_names - [stream_name], sort) %>" title="Remove from comparison">×</a>
|
|
34
|
+
<% end -%>
|
|
35
|
+
</div>
|
|
36
|
+
</th>
|
|
37
|
+
<% end -%>
|
|
38
|
+
</tr>
|
|
39
|
+
</thead>
|
|
40
|
+
<tbody data-swimlane-view-target="tbody">
|
|
41
|
+
<%= render("swimlane/_rows", urls: urls, stream_names: stream_names, events: events, sort: sort) %>
|
|
42
|
+
</tbody>
|
|
43
|
+
</table>
|
|
44
|
+
</div>
|
|
45
|
+
<% end -%>
|
|
46
|
+
</div>
|
|
47
|
+
<dialog data-swimlane-add-target="dialog" class="open:flex flex-col items-center justify-center fixed inset-0 size-full max-w-full max-h-full bg-transparent backdrop:bg-black/50">
|
|
48
|
+
<button class="fixed inset-0 -z-10 cursor-default" data-action="swimlane-add#close" tabindex="-1" aria-label="Close"></button>
|
|
49
|
+
<form data-action="swimlane-add#go" class="w-full max-w-md p-4 bg-white rounded-lg shadow-xl">
|
|
50
|
+
<div class="relative">
|
|
51
|
+
<input data-swimlane-add-target="input" class="w-full px-3 py-2 text-gray-800 rounded outline-none appearance-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50" placeholder="Add stream to compare…" autofocus>
|
|
52
|
+
</div>
|
|
53
|
+
</form>
|
|
54
|
+
</dialog>
|
|
55
|
+
</div>
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
3
5
|
module RubyEventStore
|
|
4
6
|
module Browser
|
|
5
7
|
PAGE_SIZE = 20
|
|
6
8
|
SERIALIZED_GLOBAL_STREAM_NAME = "all".freeze
|
|
7
9
|
DEFAULT_RELATED_STREAMS_QUERY = ->(stream_name) { [] }
|
|
10
|
+
|
|
11
|
+
ASSETS_ROOT = File.expand_path("../../public", __dir__).freeze
|
|
12
|
+
|
|
13
|
+
def self.fingerprint(name)
|
|
14
|
+
base, ext = name.split(".", 2)
|
|
15
|
+
digest = Digest::MD5.hexdigest(File.binread(File.join(ASSETS_ROOT, name)))[0, 8]
|
|
16
|
+
"#{base}.#{ext}?v=#{digest}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
BROWSER_JS = fingerprint("ruby_event_store_browser.js")
|
|
20
|
+
BROWSER_CSS = fingerprint("ruby_event_store_browser.css")
|
|
8
21
|
end
|
|
9
22
|
end
|
|
10
23
|
|
|
11
|
-
require_relative "browser/get_event"
|
|
12
|
-
require_relative "browser/json_api_event"
|
|
13
|
-
require_relative "browser/json_api_stream"
|
|
14
24
|
require_relative "browser/get_events_from_stream"
|
|
15
|
-
require_relative "browser/
|
|
25
|
+
require_relative "browser/get_events_from_streams"
|
|
16
26
|
require_relative "browser/urls"
|
|
17
|
-
require_relative "browser/gem_source"
|
|
18
27
|
require_relative "browser/router"
|
|
28
|
+
require_relative "browser/renderer"
|
|
29
|
+
require_relative "browser/layout"
|
|
30
|
+
require_relative "browser/extension_context"
|