snapshot_ui 0.2.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/minitest/snapshot_ui_plugin.rb +3 -1
- data/lib/snapshot_ui/cli/watcher.ru +11 -9
- data/lib/snapshot_ui/cli.rb +11 -8
- data/lib/snapshot_ui/colorize.rb +15 -0
- data/lib/snapshot_ui/configuration.rb +24 -4
- data/lib/snapshot_ui/test/minitest_helpers.rb +2 -0
- data/lib/snapshot_ui/version.rb +1 -1
- data/lib/snapshot_ui/web/application.rb +27 -0
- data/lib/snapshot_ui/web/assets/javascripts/application.js +2 -0
- data/lib/snapshot_ui/web/assets/javascripts/controllers/source_location_controller.js +17 -0
- data/lib/snapshot_ui/web/assets/stylesheets/application.css +78 -2
- data/lib/snapshot_ui/web/views/layout.html.erb +2 -1
- data/lib/snapshot_ui/web/views/snapshots/index.html.erb +11 -5
- data/lib/snapshot_ui.rb +14 -3
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5d083a6167cbd33d814565a34c6da82c32f920bf1fef6faa8171e1cf4bae1f86
|
4
|
+
data.tar.gz: 15f7d8346e5f192e4b8901760b64a1d91362507bb7adf91cbaed37e30a257613
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 07c26236a62e772945eb5165e2552d2a278493acb7b65d4429f445e98b2401b5ee39acfe842b2a5c9787220b5444ac3f0a1ecab1f7e8cb0a7f87e2f01726b2b2
|
7
|
+
data.tar.gz: 5fa30fbda712efff8c596acca55ec3811fee661a6684fa7d34aee7a30b2032255e14930c8288f90e5e5fe15d82424cef66848a4860d560241d9631e57837b94a
|
@@ -12,6 +12,8 @@ module Minitest
|
|
12
12
|
def plugin_snapshot_ui_init(_options)
|
13
13
|
return unless SnapshotUI.snapshot_taking_enabled?
|
14
14
|
|
15
|
+
SnapshotUI.exit_if_not_configured!
|
16
|
+
|
15
17
|
reporter << SnapshotUIReporter.new
|
16
18
|
|
17
19
|
SnapshotUI.clear_snapshots_in_progress
|
@@ -22,7 +24,7 @@ module Minitest
|
|
22
24
|
def report
|
23
25
|
SnapshotUI.publish_snapshots_in_progress
|
24
26
|
|
25
|
-
io.
|
27
|
+
io.puts "\n\nUI snapshots are ready for review at #{SnapshotUI.configuration.web_url}"
|
26
28
|
end
|
27
29
|
end
|
28
30
|
end
|
@@ -6,18 +6,19 @@ require "async/websocket/client"
|
|
6
6
|
require "async/websocket/adapters/rack"
|
7
7
|
require "listen"
|
8
8
|
require "set"
|
9
|
+
require "uri"
|
9
10
|
|
10
|
-
|
11
|
-
WEBSOCKET_ENDPOINT = "http://localhost:7070/cable"
|
11
|
+
load ENV.fetch("SNAPSHOT_UI_INITIALIZER_FILE")
|
12
12
|
|
13
13
|
REFRESH_MESSAGE = {identifier: "{\"channel\":\"RefreshChannel\"}", message: "refresh"}.to_json
|
14
14
|
CONFIRM_SUBSCRIPTION_MESSAGE = {identifier: "{\"channel\":\"RefreshChannel\"}", type: "confirm_subscription"}.to_json
|
15
|
-
PING_MESSAGE = {type: "ping", message: Time.now.to_i.to_s}.to_json
|
16
15
|
|
17
16
|
@connections = Set.new
|
18
17
|
|
18
|
+
live_websocket_uri = URI.parse(SnapshotUI.configuration.live_websocket_url)
|
19
|
+
|
19
20
|
run lambda { |env|
|
20
|
-
if env["REQUEST_PATH"] ==
|
21
|
+
if env["REQUEST_PATH"] == live_websocket_uri.path
|
21
22
|
Async::WebSocket::Adapters::Rack.open(env, protocols: ["actioncable-v1-json"]) do |connection|
|
22
23
|
@connections << connection
|
23
24
|
|
@@ -27,7 +28,7 @@ run lambda { |env|
|
|
27
28
|
|
28
29
|
Async do |ping_task|
|
29
30
|
loop do
|
30
|
-
connection.write(
|
31
|
+
connection.write({type: "ping", message: Time.now.to_i.to_s}.to_json)
|
31
32
|
connection.flush
|
32
33
|
sleep(2)
|
33
34
|
end
|
@@ -50,7 +51,7 @@ run lambda { |env|
|
|
50
51
|
}
|
51
52
|
|
52
53
|
Async do |client_task|
|
53
|
-
endpoint = Async::HTTP::Endpoint.parse(
|
54
|
+
endpoint = Async::HTTP::Endpoint.parse(SnapshotUI.configuration.live_websocket_url)
|
54
55
|
|
55
56
|
Async::WebSocket::Client.connect(endpoint) do |connection|
|
56
57
|
Async do |listener_task|
|
@@ -68,15 +69,16 @@ Async do |client_task|
|
|
68
69
|
end
|
69
70
|
|
70
71
|
def detect_snapshots_update(task)
|
71
|
-
Pathname.new(
|
72
|
-
listener = Listen.to(
|
72
|
+
Pathname.new(SnapshotUI.configuration.storage_directory).mkpath
|
73
|
+
listener = Listen.to(SnapshotUI.configuration.storage_directory) { |_modified, _added, _removed| broadcast_update }
|
73
74
|
|
74
75
|
task.async do
|
75
76
|
listener.start
|
76
77
|
task.sleep
|
77
78
|
end
|
78
79
|
|
79
|
-
Console.info("Watching for snapshots updates in #{
|
80
|
+
Console.info("Watching for snapshots updates in #{SnapshotUI.configuration.storage_directory}")
|
81
|
+
Console.info("Review snapshots on #{SnapshotUI.configuration.web_url}")
|
80
82
|
end
|
81
83
|
|
82
84
|
def broadcast_update
|
data/lib/snapshot_ui/cli.rb
CHANGED
@@ -1,22 +1,25 @@
|
|
1
1
|
require "bundler/setup"
|
2
2
|
require "thor"
|
3
3
|
require "pathname"
|
4
|
+
require "uri"
|
4
5
|
|
5
6
|
module SnapshotUI
|
6
7
|
class CLI < Thor
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
unless File.exist?(snapshot_directory)
|
12
|
-
puts "The provided directory `#{snapshot_directory}` doesn't exist. Please double check the path."
|
8
|
+
desc "live SNAPSHOT_UI_INITIALIZER_FILE", "Run the command to enable live refreshing of UI snapshots."
|
9
|
+
def live(initializer_file)
|
10
|
+
unless File.exist?(initializer_file)
|
11
|
+
puts "The provided initializer file `#{initializer_file}` doesn't exist. Please double check the path."
|
13
12
|
exit 1
|
14
13
|
end
|
15
14
|
|
16
|
-
|
15
|
+
load initializer_file
|
16
|
+
|
17
|
+
websocket_host_uri = URI.parse(SnapshotUI.configuration.live_websocket_url)
|
18
|
+
websocket_host_uri.path = ""
|
19
|
+
|
17
20
|
config_path = Pathname.new(__dir__).join("cli", "watcher.ru").cleanpath.to_s
|
18
21
|
|
19
|
-
exec "
|
22
|
+
exec "SNAPSHOT_UI_INITIALIZER_FILE=#{initializer_file} bundle exec falcon serve --bind #{websocket_host_uri} --count 1 --config #{config_path}"
|
20
23
|
end
|
21
24
|
|
22
25
|
def self.exit_on_failure?
|
@@ -1,22 +1,42 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "colorize"
|
4
|
+
|
3
5
|
module SnapshotUI
|
4
6
|
class Configuration
|
5
7
|
attr_writer :storage_directory, :project_root_directory
|
6
|
-
attr_accessor :web_url
|
8
|
+
attr_accessor :web_url, :live_websocket_url
|
7
9
|
|
8
|
-
def initialize(project_root_directory:, storage_directory:, web_url:)
|
10
|
+
def initialize(project_root_directory:, storage_directory:, web_url:, live_websocket_url:)
|
9
11
|
@project_root_directory = project_root_directory
|
10
12
|
@storage_directory = storage_directory
|
11
13
|
@web_url = web_url
|
14
|
+
@live_websocket_url = live_websocket_url
|
12
15
|
end
|
13
16
|
|
14
17
|
def storage_directory
|
15
|
-
Pathname.new(@storage_directory)
|
18
|
+
Pathname.new(@storage_directory) if @storage_directory
|
16
19
|
end
|
17
20
|
|
18
21
|
def project_root_directory
|
19
|
-
Pathname.new(@project_root_directory)
|
22
|
+
Pathname.new(@project_root_directory) if @project_root_directory
|
23
|
+
end
|
24
|
+
|
25
|
+
def exit_if_not_configured!
|
26
|
+
return unless project_root_directory.nil? || storage_directory.nil? || web_url.nil?
|
27
|
+
|
28
|
+
puts Colorize.red("Looks like SnapshotUI is not configured yet. Example configuration:\n")
|
29
|
+
|
30
|
+
puts <<~CONFIG
|
31
|
+
#{Colorize.green("SnapshotUI.configure do |config|")}
|
32
|
+
#{Colorize.green('config.storage_directory = "/path/to/tmp/snapshot_ui"')} #{Colorize.red("# Current value is `#{storage_directory.inspect}`")}
|
33
|
+
#{Colorize.green('config.project_root_directory = "/path/to/project/root"')} #{Colorize.red("# Current value is `#{project_root_directory.inspect}`")}
|
34
|
+
#{Colorize.green("config.web_url = \"#{web_url}\"")} #{Colorize.red("# Current value is `#{web_url.inspect}`")}
|
35
|
+
#{Colorize.green("end")}
|
36
|
+
|
37
|
+
CONFIG
|
38
|
+
|
39
|
+
raise SystemExit.new(1)
|
20
40
|
end
|
21
41
|
end
|
22
42
|
end
|
data/lib/snapshot_ui/version.rb
CHANGED
@@ -119,6 +119,33 @@ module SnapshotUI
|
|
119
119
|
def refresh_controller
|
120
120
|
'data-controller="refresh" data-action="refresh-connected@window->refresh#connected refresh-disconnected@window->refresh#disconnected turbo:before-render@window->refresh#display_status"'
|
121
121
|
end
|
122
|
+
|
123
|
+
def snapshot_title(snapshot)
|
124
|
+
title = snapshot.context.name.sub("test_", "").gsub(/^\d{4}\s*/, "").tr("_", " ")
|
125
|
+
suffix =
|
126
|
+
if snapshot.context.take_snapshot_index > 0
|
127
|
+
" (##{snapshot.context.take_snapshot_index + 1} in the same test)"
|
128
|
+
end
|
129
|
+
|
130
|
+
"#{title}#{suffix}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def test_group_title(test_group)
|
134
|
+
parts = test_group.split("::")
|
135
|
+
last_part = "<span class='last'>#{parts.last}</span>"
|
136
|
+
all = parts[0..-2] << last_part
|
137
|
+
|
138
|
+
all.join(" <span class='divider'>/</span> ")
|
139
|
+
end
|
140
|
+
|
141
|
+
def copy_icon_svg
|
142
|
+
<<~HTML
|
143
|
+
<svg class="copy icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
144
|
+
<!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
145
|
+
<path fill="currentColor" d="M384 336l-192 0c-8.8 0-16-7.2-16-16l0-256c0-8.8 7.2-16 16-16l140.1 0L400 115.9 400 320c0 8.8-7.2 16-16 16zM192 384l192 0c35.3 0 64-28.7 64-64l0-204.1c0-12.7-5.1-24.9-14.1-33.9L366.1 14.1c-9-9-21.2-14.1-33.9-14.1L192 0c-35.3 0-64 28.7-64 64l0 256c0 35.3 28.7 64 64 64zM64 128c-35.3 0-64 28.7-64 64L0 448c0 35.3 28.7 64 64 64l192 0c35.3 0 64-28.7 64-64l0-32-48 0 0 32c0 8.8-7.2 16-16 16L64 464c-8.8 0-16-7.2-16-16l0-256c0-8.8 7.2-16 16-16l32 0 0-48-32 0z"/>
|
146
|
+
</svg>
|
147
|
+
HTML
|
148
|
+
end
|
122
149
|
end
|
123
150
|
end
|
124
151
|
end
|
@@ -3,6 +3,8 @@ import "@hotwired/turbo"
|
|
3
3
|
import "channels"
|
4
4
|
|
5
5
|
import RefreshController from "controllers/refresh_controller"
|
6
|
+
import SourceLocationController from "controllers/source_location_controller"
|
6
7
|
|
7
8
|
window.Stimulus = Application.start()
|
8
9
|
Stimulus.register("refresh", RefreshController)
|
10
|
+
Stimulus.register("source-location", SourceLocationController)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
copy(event) {
|
5
|
+
event.preventDefault()
|
6
|
+
|
7
|
+
let currentTarget = event.currentTarget
|
8
|
+
|
9
|
+
navigator.clipboard.writeText(currentTarget.dataset.sourceLocation)
|
10
|
+
|
11
|
+
currentTarget.classList.toggle('animate');
|
12
|
+
setTimeout(()=> {
|
13
|
+
currentTarget.classList.toggle('animate')
|
14
|
+
},300)
|
15
|
+
|
16
|
+
}
|
17
|
+
}
|
@@ -28,6 +28,15 @@ body#snapshots_index h1 {
|
|
28
28
|
body#snapshots_index h2 {
|
29
29
|
margin: 30px 0 0;
|
30
30
|
font-size: 24px;
|
31
|
+
color: #aaa;
|
32
|
+
}
|
33
|
+
|
34
|
+
body#snapshots_index h2 .last {
|
35
|
+
color: #666666;
|
36
|
+
}
|
37
|
+
|
38
|
+
body#snapshots_index h2 .divider {
|
39
|
+
color: #ccc;
|
31
40
|
}
|
32
41
|
|
33
42
|
body#snapshots_index p {
|
@@ -44,8 +53,14 @@ body#snapshots_index h1, h2, h3, h4, h5, h6 {
|
|
44
53
|
color: #666666;
|
45
54
|
}
|
46
55
|
|
47
|
-
body#snapshots_index a {
|
48
|
-
|
56
|
+
body#snapshots_index a {
|
57
|
+
color: #000;
|
58
|
+
text-decoration: none;
|
59
|
+
}
|
60
|
+
|
61
|
+
body#snapshots_index a:hover {
|
62
|
+
text-decoration: underline;
|
63
|
+
}
|
49
64
|
|
50
65
|
body#snapshots_index code {
|
51
66
|
background: rgba(175, 184, 193, 0.2);
|
@@ -101,6 +116,67 @@ body#not_found p {
|
|
101
116
|
color: #666666;
|
102
117
|
}
|
103
118
|
|
119
|
+
.copy_source_location {
|
120
|
+
all: unset;
|
121
|
+
cursor: pointer;
|
122
|
+
position: relative;
|
123
|
+
}
|
124
|
+
|
125
|
+
.copy_source_location .icon {
|
126
|
+
width: 0.9em;
|
127
|
+
height: 0.9em;
|
128
|
+
color: #aaa;
|
129
|
+
}
|
130
|
+
|
131
|
+
.copy_source_location:hover .icon {
|
132
|
+
color: #000;
|
133
|
+
}
|
134
|
+
|
135
|
+
.copy_source_location.animate .icon {
|
136
|
+
animation: enlarge-and-back-to-normal 0.3s;
|
137
|
+
}
|
138
|
+
|
139
|
+
@keyframes enlarge-and-back-to-normal {
|
140
|
+
0%,
|
141
|
+
100% {
|
142
|
+
transform: scale(1, 1);
|
143
|
+
}
|
144
|
+
50% {
|
145
|
+
transform: scale(1.3, 1.3);
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
.copy_source_location:before {
|
150
|
+
content: attr(data-source-location);
|
151
|
+
position:absolute;
|
152
|
+
font-size: 90%;
|
153
|
+
top:-95%;
|
154
|
+
transform:translateX(-50%);
|
155
|
+
left:100%;
|
156
|
+
margin-top:-15px;
|
157
|
+
padding:3px 15px 1px;
|
158
|
+
border-radius:10px;
|
159
|
+
background:#eee;
|
160
|
+
color: #666;
|
161
|
+
text-align:center;
|
162
|
+
display:none;
|
163
|
+
}
|
164
|
+
|
165
|
+
.copy_source_location:after {
|
166
|
+
content: "";
|
167
|
+
position:absolute;
|
168
|
+
top:-11px;
|
169
|
+
transform:translateX(-50%);
|
170
|
+
margin-left:10px;
|
171
|
+
border:10px solid #eee;
|
172
|
+
border-color: #eee transparent transparent transparent;
|
173
|
+
display:none;
|
174
|
+
}
|
175
|
+
|
176
|
+
.copy_source_location:hover:before, .copy_source_location:hover:after {
|
177
|
+
display:block;
|
178
|
+
}
|
179
|
+
|
104
180
|
/* Hotwired Turbo related */
|
105
181
|
.turbo-progress-bar {
|
106
182
|
height: 2px;
|
@@ -7,7 +7,7 @@
|
|
7
7
|
<meta name="turbo-refresh-method" content="morph">
|
8
8
|
<meta name="turbo-refresh-scroll" content="preserve">
|
9
9
|
<link rel="stylesheet" href="<%= stylesheet_path("application.css") %>">
|
10
|
-
<meta name="action-cable-url" content="
|
10
|
+
<meta name="action-cable-url" content="<%= SnapshotUI.configuration.live_websocket_url %>" />
|
11
11
|
<script type="importmap" data-turbo-track="reload">
|
12
12
|
{
|
13
13
|
"imports": {
|
@@ -16,6 +16,7 @@
|
|
16
16
|
"@hotwired/turbo": "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.4/+esm",
|
17
17
|
"@rails/actioncable": "https://cdn.jsdelivr.net/npm/@rails/actioncable@7.1.3-4/+esm",
|
18
18
|
"controllers/refresh_controller": "<%= javascript_path("controllers/refresh_controller.js") %>",
|
19
|
+
"controllers/source_location_controller": "<%= javascript_path("controllers/source_location_controller.js") %>",
|
19
20
|
"channels/consumer": "<%= javascript_path("channels/consumer.js") %>",
|
20
21
|
"channels": "<%= javascript_path("channels/index.js") %>",
|
21
22
|
"channels/refresh_channel": "<%= javascript_path("channels/refresh_channel.js") %>"
|
@@ -44,15 +44,21 @@ bundle exec snapshot_ui watch SNAPSHOT_DIRECTORY</pre>
|
|
44
44
|
<% end %>
|
45
45
|
|
46
46
|
<% @grouped_by_test_class.each do |test_group, snapshots| %>
|
47
|
-
|
47
|
+
|
48
|
+
<h2><%= test_group_title(test_group) %></h2>
|
48
49
|
|
49
50
|
<ul>
|
50
51
|
<% snapshots.each do |snapshot| %>
|
51
52
|
<li>
|
52
|
-
<a href="<%= snapshot_path(snapshot.slug) %>">
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
<a href="<%= snapshot_path(snapshot.slug) %>"><%= snapshot_title(snapshot) %></a>
|
54
|
+
<button
|
55
|
+
class="copy_source_location"
|
56
|
+
data-controller="source-location"
|
57
|
+
data-action="click->source-location#copy"
|
58
|
+
data-source-location="<%= snapshot.context.source_location.join(":") %>"
|
59
|
+
>
|
60
|
+
<%= copy_icon_svg %>
|
61
|
+
</button>
|
56
62
|
</li>
|
57
63
|
<% end %>
|
58
64
|
</ul>
|
data/lib/snapshot_ui.rb
CHANGED
@@ -5,9 +5,10 @@ require_relative "snapshot_ui/configuration"
|
|
5
5
|
|
6
6
|
module SnapshotUI
|
7
7
|
DEFAULT_CONFIGURATION = {
|
8
|
-
project_root_directory:
|
9
|
-
storage_directory:
|
10
|
-
web_url: "http://localhost:3000/ui/snapshots"
|
8
|
+
project_root_directory: nil,
|
9
|
+
storage_directory: nil,
|
10
|
+
web_url: "http://localhost:3000/ui/snapshots",
|
11
|
+
live_websocket_url: "http://localhost:49152/live"
|
11
12
|
}.freeze
|
12
13
|
|
13
14
|
def self.configure
|
@@ -23,14 +24,24 @@ module SnapshotUI
|
|
23
24
|
end
|
24
25
|
|
25
26
|
def self.publish_snapshots_in_progress
|
27
|
+
exit_if_not_configured!
|
28
|
+
|
26
29
|
Snapshot.publish_snapshots_in_progress
|
27
30
|
end
|
28
31
|
|
29
32
|
def self.clear_snapshots_in_progress
|
33
|
+
exit_if_not_configured!
|
34
|
+
|
30
35
|
Snapshot.clear_snapshots_in_progress
|
31
36
|
end
|
32
37
|
|
33
38
|
def self.clear_snapshots
|
39
|
+
exit_if_not_configured!
|
40
|
+
|
34
41
|
Snapshot.clear_snapshots
|
35
42
|
end
|
43
|
+
|
44
|
+
def self.exit_if_not_configured!
|
45
|
+
configuration.exit_if_not_configured!
|
46
|
+
end
|
36
47
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: snapshot_ui
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tomaz Zlender
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-07-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -96,6 +96,7 @@ files:
|
|
96
96
|
- lib/snapshot_ui.rb
|
97
97
|
- lib/snapshot_ui/cli.rb
|
98
98
|
- lib/snapshot_ui/cli/watcher.ru
|
99
|
+
- lib/snapshot_ui/colorize.rb
|
99
100
|
- lib/snapshot_ui/configuration.rb
|
100
101
|
- lib/snapshot_ui/snapshot.rb
|
101
102
|
- lib/snapshot_ui/snapshot/context.rb
|
@@ -109,6 +110,7 @@ files:
|
|
109
110
|
- lib/snapshot_ui/web/assets/javascripts/channels/index.js
|
110
111
|
- lib/snapshot_ui/web/assets/javascripts/channels/refresh_channel.js
|
111
112
|
- lib/snapshot_ui/web/assets/javascripts/controllers/refresh_controller.js
|
113
|
+
- lib/snapshot_ui/web/assets/javascripts/controllers/source_location_controller.js
|
112
114
|
- lib/snapshot_ui/web/assets/stylesheets/application.css
|
113
115
|
- lib/snapshot_ui/web/views/layout.html.erb
|
114
116
|
- lib/snapshot_ui/web/views/snapshots/index.html.erb
|