restyle log entry page

This commit is contained in:
Dylan Knutson
2025-02-26 00:34:40 +00:00
parent a296688123
commit ff4c374453
9 changed files with 306 additions and 152 deletions

View File

@@ -43,3 +43,11 @@
100% 10px; 100% 10px;
background-attachment: local, local, scroll, scroll; background-attachment: local, local, scroll, scroll;
} }
.log-entry-table-header-cell {
@apply border-b border-slate-200 bg-slate-50 px-2 py-1 text-xs font-medium uppercase tracking-wider text-slate-500;
}
.log-entry-table-row-cell {
@apply flex items-center border-b border-slate-200 px-2 py-1 text-sm group-hover:bg-slate-50;
}

View File

@@ -76,7 +76,7 @@ class LogEntriesController < ApplicationController
HttpLogEntry.includes( HttpLogEntry.includes(
:caused_by_entry, :caused_by_entry,
:triggered_entries, :triggered_entries,
response: :base, :response,
).find(params[:id]) ).find(params[:id])
end end
end end

View File

@@ -36,67 +36,68 @@
<% end %> <% end %>
</div> </div>
<%= render partial: "shared/pagination_controls", locals: { collection: @log_entries } %> <%= render partial: "shared/pagination_controls", locals: { collection: @log_entries } %>
<div class="grid grid-cols-[auto_auto_auto_auto_1fr_auto_auto_auto] border-b border-slate-300 text-sm max-w-screen-lg mx-auto"> <div class="grid grid-cols-[auto_auto_auto_auto_1fr_auto_auto_auto] max-w-screen-lg mx-auto overflow-hidden border border-slate-200 bg-white shadow mb-4 rounded-lg">
<div class='grid-row contents'> <div class='contents'>
<div class="grid-cell text-center font-semibold">ID</div> <div class="log-entry-table-header-cell text-center rounded-tl">ID</div>
<div class="grid-cell text-right font-semibold">Size</div> <div class="log-entry-table-header-cell text-right">Size</div>
<div class="grid-cell text-center font-semibold">Time</div> <div class="log-entry-table-header-cell text-center">Time</div>
<div class="grid-cell text-center font-semibold">Status</div> <div class="log-entry-table-header-cell text-center">Status</div>
<div class="grid-cell text-center font-semibold">URI</div> <div class="log-entry-table-header-cell text-center">URI</div>
<div class="grid-cell text-center font-semibold"></div> <div class="log-entry-table-header-cell text-center"><!-- External link --></div>
<div class="grid-cell text-center font-semibold">Type</div> <div class="log-entry-table-header-cell text-left">Type</div>
<div class="grid-cell text-center font-semibold">Response</div> <div class="log-entry-table-header-cell text-right rounded-tr">Resp</div>
</div> </div>
<div class="col-span-full border-b border-slate-300"></div>
<% @log_entries.each do |hle| %> <% @log_entries.each do |hle| %>
<div class="grid-row contents"> <div class="contents group">
<div class="grid-cell text-center"> <div class="log-entry-table-row-cell justify-end">
<%= link_to hle.id, log_entry_path(hle.id), class: "text-blue-600 hover:text-blue-800" %> <%= link_to hle.id, log_entry_path(hle.id), class: "text-blue-600 hover:text-blue-800 font-medium" %>
</div> </div>
<div class="grid-cell text-right"> <div class="log-entry-table-row-cell justify-end">
<%= HexUtil.humansize(hle.response.size) %> <%= HexUtil.humansize(hle.response_size) %>
</div> </div>
<div class="grid-cell text-right"> <div class="log-entry-table-row-cell text-right">
<%= time_ago_in_words(hle.created_at, include_seconds: true) %> ago <%= time_ago_in_words(hle.created_at, include_seconds: true) %> ago
</div> </div>
<div class="grid-cell text-center"> <div class="log-entry-table-row-cell text-center">
<span class="<%= hle.status_code == 200 ? 'text-green-600' : 'text-red-600' %>"> <span class="<%= hle.status_code == 200 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' %> px-2 py-1 rounded-full text-xs font-medium">
<%= hle.status_code %> <%= hle.status_code %>
</span> </span>
</div> </div>
<div class="grid-cell min-w-0"> <div class="log-entry-table-row-cell text-right min-w-0">
<% iterative_parts = path_iterative_parts(hle.uri_path) %> <% iterative_parts = path_iterative_parts(hle.uri_path) %>
<div class="flex truncate whitespace-nowrap overflow-hidden "> <div class="flex items-center px-2 space-x-0 bg-slate-100 rounded group-hover:border-slate-700 border truncate group-hover:overflow-visible group-hover:z-10 relative">
<a class="bg-slate-100 rounded-l hover:bg-slate-200" href="/log_entries/filter/<%= hle.uri_host %>"> <a class="py-1 bg-slate-100 rounded-l hover:bg-slate-200 transition-colors" href="/log_entries/filter/<%= hle.uri_host %>">
<%= hle.uri_scheme %>://<%= hle.uri_host %> <span class="text-slate-600"><%= hle.uri_scheme %>://</span><span class="font-medium"><%= hle.uri_host %></span>
</a> </a>
<%- iterative_parts.each_with_index do |(part, up_to), index| -%> <%- iterative_parts.each_with_index do |(part, up_to), index| -%>
<% uri_and_up_to = hle.uri_host + up_to %> <% uri_and_up_to = hle.uri_host + up_to %>
<a <a
class="bg-slate-100 <%= index == iterative_parts.length-1 ? 'rounded-r' : '' %> hover:bg-slate-200" class="py-1 hover:bg-slate-200 transition-colors"
href="/log_entries/filter/<%= uri_and_up_to %>" href="/log_entries/filter/<%= uri_and_up_to %>"
title="<%= hle.uri_scheme + "://" + uri_and_up_to %>" title="<%= hle.uri_scheme + "://" + uri_and_up_to %>"
>/<%= part %></a> >/<span class="font-medium text-nowrap"><%= part %></span></a>
<%- end -%> <%- end -%>
<%- if hle.uri_query -%> <%- if hle.uri_query -%>
<% query_parsed = URI.decode_www_form(hle.uri_query).to_h %> <% query_parsed = URI.decode_www_form(hle.uri_query).to_h %>
<span class="text-slate-600 min-w-0" title="<%= query_parsed.pretty_inspect %>"> <span class="py-1 text-slate-500" title="<%= query_parsed.pretty_inspect %>">
<%= "?#{hle.uri_query}" %> ?<%= hle.uri_query %>
</span><%- end -%> </span>
<%- end -%>
</div> </div>
</div> </div>
<div class="grid-cell text-center"> <div class="log-entry-table-row-cell text-center">
<%= link_to hle.uri.to_s, class: "text-blue-600 hover:text-blue-800", target: "_blank", rel: "noreferrer" do %> <%= link_to hle.uri.to_s, class: "text-blue-600 hover:text-blue-800 transition-colors", target: "_blank", rel: "noreferrer" do %>
<%= render partial: "shared/icons/external_link", locals: { class_name: "w-4 h-4 inline" } %> <%= render partial: "shared/icons/external_link", locals: { class_name: "w-4 h-4" } %>
<% end %> <% end %>
</div> </div>
<div class="grid-cell max-w-24 truncate"> <div class="log-entry-table-row-cell">
<span title="<%= hle.content_type %>"> <%= hle.content_type %> </span> <span class="max-w-24 truncate inline-block" title="<%= hle.content_type %>">
<%= hle.content_type %>
</span>
</div> </div>
<div class="grid-cell text-right"> <div class="justify-end log-entry-table-row-cell">
<%= hle.response_time_ms %>ms <%= hle.response_time_ms %>ms
</div> </div>
</div> </div>
<div class="col-span-full border-b border-slate-300"></div>
<% end %> <% end %>
</div> </div>

View File

@@ -17,16 +17,43 @@
</style> </style>
<% end %> <% end %>
<div class="bg-slate-50"> <div class="bg-slate-50">
<div class="mx-auto flex max-w-screen-md flex-row py-4"> <div class="mx-auto flex flex-col sm:flex-row max-w-screen-md py-4">
<div class="ml-4 flex min-w-[150px] items-center pr-4"> <div class="ml-4 flex min-w-[150px] items-center pr-4 mb-2 sm:mb-0 justify-between">
<%= link_to log_entries_path, <%= link_to log_entries_path,
class: class:
"px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-md hover:bg-slate-50" do %> "px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-md hover:bg-slate-50" do %>
<span class="mr-1">&larr;</span> <span class="mr-1">&larr;</span>
<span>Log Entries</span> <span>Log Entries</span>
<% end %> <% end %>
<button type="button"
onclick="toggleDetails(this)"
class="sm:hidden ml-2 px-2 py-1 text-sm text-slate-700 bg-white border border-slate-300 rounded-md hover:bg-slate-50">
<span id="expandIcon" class="hidden">▼</span>
<span id="collapseIcon">▲</span>
</button>
<script>
function toggleDetails(button) {
const expandIcon = button.querySelector('#expandIcon');
const collapseIcon = button.querySelector('#collapseIcon');
const details = document.getElementById('log-entry-details');
expandIcon.classList.toggle('hidden');
collapseIcon.classList.toggle('hidden');
details.classList.toggle('shadow-[inset_0_-5px_5px_-5px_rgba(0,0,0,0.2)]');
if (expandIcon.classList.contains('hidden')) {
details.style.maxHeight = details.scrollHeight + 'px';
} else {
details.style.maxHeight = '100px';
}
}
window.addEventListener('DOMContentLoaded', () => {
const details = document.getElementById('log-entry-details');
details.style.maxHeight = details.scrollHeight + 'px';
});
</script>
</div> </div>
<div class="rounded-lg bg-white p-4 shadow"> <div id="log-entry-details" class="sm:rounded-lg bg-white p-4 shadow transition-[max-height] duration-300 overflow-hidden">
<div class="grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2"> <div class="grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2">
<div class="text-right text-sm text-slate-500">URL</div> <div class="text-right text-sm text-slate-500">URL</div>
<div> <div>
@@ -61,11 +88,7 @@
><%= rtms == -1 ? "Response time not recorded" : "#{rtms}ms" %></span ><%= rtms == -1 ? "Response time not recorded" : "#{rtms}ms" %></span
> >
<span class="mx-2">•</span> <span class="mx-2">•</span>
<span <span><%= HexUtil.humansize(@log_entry.response.size_bytes) %></span>
><%= HexUtil.humansize(@log_entry.response.size) %>
(<%= HexUtil.humansize(@log_entry.response.bytes_stored) %>
stored)</span
>
<span class="mx-2">•</span> <span class="mx-2">•</span>
<span>Performed by <%= @log_entry.performed_by %></span> <span>Performed by <%= @log_entry.performed_by %></span>
<span class="mx-2">•</span> <span class="mx-2">•</span>
@@ -88,17 +111,6 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<% if @log_entry.response.base %>
<div class="text-right text-sm text-slate-500">Base Entry</div>
<div>
<% base_hle = HttpLogEntry.find_by(response: @log_entry.response.base) %>
<% if base_hle %>
<%= render partial: "log_entry_table_row_mini", locals: { entry: base_hle } %>
<% else %>
<span class="italic text-slate-500">HLE not found...</span>
<% end %>
</div>
<% end %>
<% if @log_entry.caused_by_entry %> <% if @log_entry.caused_by_entry %>
<div class="text-right text-sm text-slate-500">Caused By</div> <div class="text-right text-sm text-slate-500">Caused By</div>
<div class="overflow-hidden rounded border border-slate-200"> <div class="overflow-hidden rounded border border-slate-200">

View File

@@ -1,121 +1,216 @@
<% content_for :head do %> <% content_for :head do %>
<style type="text/css" data-turbolinks-track> <style type="text/css" data-turbolinks-track>
.grid-cell { .grid-cell {
padding: 0.25rem; padding: 0.25rem 1rem;
border-right: 1px solid #e2e8f0; border-right: 1px solid #e2e8f0;
} }
.grid-cell:last-child { .grid-cell:last-child {
padding-left: 0;
padding-right: 1rem;
border-right: none; border-right: none;
} }
.grid-cell:first-child {
padding-left: 1rem;
}
.grid-row:hover .grid-cell { .grid-row:hover .grid-cell {
background-color: #f1f5f9; background-color: #f1f5f9;
transition: background-color 150ms ease-in-out;
}
.stats-card {
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
background-color: white;
padding: 1rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.stats-grid {
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
background-color: white;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
overflow: hidden;
}
.stats-grid-header {
background-color: #f8fafc;
padding: 0.75rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: #334155;
cursor: pointer;
user-select: none;
}
.stats-grid-header:hover {
background-color: #f1f5f9;
}
.sort-indicator::after {
content: "▼";
font-size: 0.75rem;
margin-left: 0.25rem;
opacity: 0.5;
}
.sort-indicator.asc::after {
content: "▲";
} }
</style> </style>
<% end %> <% end %>
<div class="mx-auto max-w-5xl px-4 mt-6 sm:mt-8">
<div class="mx-auto mt-4 text-center sm:mt-6"> <h1 class="text-2xl font-bold text-slate-900 text-center">HTTP Request Log Stats</h1>
<h1 class="text-2xl">Http Request Log Stats</h1> <div class="mt-2 text-center">
<div class="mt-2 text-lg">
<%= link_to "Back to Index", <%= link_to "Back to Index",
log_entries_path, log_entries_path,
class: "text-blue-600 hover:text-blue-800" %> class: "text-blue-600 hover:text-blue-800 transition-colors" %>
</div> </div>
</div> <div class="mt-6 text-center">
<div class="space-x-2">
<div class="mx-auto mt-4 text-center"> <% [10.seconds, 30.seconds, 1.minute, 5.minutes, 30.minutes].each do |time_window| %>
<div class="space-x-2 text-lg"> <% if @time_window == time_window %>
<% [10.seconds, 30.seconds, 1.minute, 5.minutes, 30.minutes].each do |time_window| %> <span class="rounded-full bg-blue-100 px-3 py-1 text-sm font-medium text-blue-800">
<% if @time_window == time_window %> <%= if time_window < 1.minute
<span class="rounded bg-blue-100 px-2 py-1">
<%= if time_window < 1.minute
pluralize(time_window.seconds, "second")
else
time_ago_in_words(time_window.ago)
end %>
</span>
<% else %>
<%= link_to(
(
if time_window < 1.minute
pluralize(time_window.seconds, "second") pluralize(time_window.seconds, "second")
else else
time_ago_in_words(time_window.ago) time_ago_in_words(time_window.ago)
end end %>
), </span>
stats_log_entries_path(seconds: time_window.in_seconds), <% else %>
class: "text-blue-600 hover:text-blue-800 hover:bg-blue-50 px-2 py-1 rounded", <%= link_to(
) %> (
if time_window < 1.minute
pluralize(time_window.seconds, "second")
else
time_ago_in_words(time_window.ago)
end
),
stats_log_entries_path(seconds: time_window.in_seconds),
class: "rounded-full px-3 py-1 text-sm text-blue-600 hover:text-blue-800 hover:bg-blue-50 transition-colors",
) %>
<% end %>
<% end %> <% end %>
<% end %>
</div>
<div class="mt-4 text-lg">
<%= @last_window_count %> requests in last
<%= if @time_window < 1.minute
pluralize(@time_window.seconds, "second")
else
time_ago_in_words(@time_window.ago)
end %>
<span class="text-slate-600">
(<%= (@last_window_count.to_f / @time_window.in_seconds).round(1) %>/sec)
</span>
</div>
<div class="mt-2 text-slate-600">
<%= HexUtil.humansize(@last_window_bytes) %> bytes transferred •
<%= HexUtil.humansize(@last_window_bytes / @time_window.in_seconds) %>/sec
</div>
</div>
<div class="mx-auto mt-6">
<h2 class="mb-2 text-xl">By content type</h2>
<div class="grid grid-cols-[1fr_auto_auto] border-b border-slate-300 text-sm">
<div class="grid-row contents">
<div class="grid-cell font-semibold">Content Type</div>
<div class="grid-cell text-right font-semibold">Requests</div>
<div class="grid-cell text-right font-semibold">Transferred</div>
</div> </div>
<div class="col-span-full border-b border-slate-300"></div> <div class="stats-card mt-6">
<% @content_type_counts <div class="text-xl font-bold text-slate-900">
.sort_by { |_ignore, stats| -stats[:count] } <%= @last_window_count %> requests
.each do |content_type, stats| %> <span class="text-base font-normal text-slate-600">
<div class="grid-row contents"> in last
<div class="grid-cell"><%= content_type %></div> <%= if @time_window < 1.minute
<div class="grid-cell text-right"><%= stats[:count] %></div> pluralize(@time_window.seconds, "second")
<div class="grid-cell text-right"> else
<%= HexUtil.humansize(stats[:bytes]) %> time_ago_in_words(@time_window.ago)
</div> end %>
</span>
</div>
<div class="mt-1 text-slate-600">
<span class="font-medium"><%= (@last_window_count.to_f / @time_window.in_seconds).round(1) %></span> requests/sec
</div>
<div class="mt-1 text-sm text-slate-600">
<span class="font-medium"><%= HexUtil.humansize(@last_window_bytes) %></span> transferred •
<span class="font-medium"><%= HexUtil.humansize(@last_window_bytes / @time_window.in_seconds) %></span>/sec
</div> </div>
<div class="col-span-full border-b border-slate-300"></div>
<% end %>
</div>
</div>
<div class="mx-auto mt-6">
<h2 class="mb-2 text-xl">By domain</h2>
<div class="grid grid-cols-[1fr_auto_auto] border-b border-slate-300 text-sm">
<div class="grid-row contents">
<div class="grid-cell font-semibold">Domain</div>
<div class="grid-cell text-right font-semibold">Requests</div>
<div class="grid-cell text-right font-semibold">Transferred</div>
</div> </div>
<div class="col-span-full border-b border-slate-300"></div> </div>
<% @by_domain_counts <div class="mt-8">
.sort_by { |_ignore, stats| -stats[:bytes] } <h2 class="text-xl font-bold text-slate-900 mb-3">By Content Type</h2>
.each do |domain, stats| %> <div class="stats-grid">
<div class="grid-row contents"> <div class="grid grid-cols-[1fr_auto_auto] sortable-table" id="content-type-table">
<div class="grid-cell"><%= domain %></div> <div class="grid-row contents">
<div class="grid-cell text-right"><%= stats[:count] %></div> <div class="stats-grid-header sort-indicator" data-column="content_type">Content Type</div>
<div class="grid-cell text-right"> <div class="stats-grid-header text-right sort-indicator" data-column="count">Requests</div>
<%= HexUtil.humansize(stats[:bytes]) %> <div class="stats-grid-header text-right sort-indicator" data-column="bytes">Transferred</div>
</div> </div>
<% @content_type_counts
.sort_by { |_ignore, stats| -stats[:count] }
.each do |content_type, stats| %>
<div class="grid-row contents" data-content-type="<%= content_type %>" data-count="<%= stats[:count] %>" data-bytes="<%= stats[:bytes] %>">
<div class="grid-cell font-medium text-slate-900 text-sm"><%= content_type %></div>
<div class="grid-cell text-right text-slate-600 text-sm"><%= stats[:count] %></div>
<div class="grid-cell text-right text-slate-600 font-medium text-sm">
<%= HexUtil.humansize(stats[:bytes]) %>
</div>
</div>
<% end %>
</div> </div>
<div class="col-span-full border-b border-slate-300"></div> </div>
<% end %> </div>
<div class="mt-8 mb-8">
<h2 class="text-xl font-bold text-slate-900 mb-3">By Domain</h2>
<div class="stats-grid">
<div class="grid grid-cols-[1fr_auto_auto] sortable-table" id="domain-table">
<div class="grid-row contents">
<div class="stats-grid-header sort-indicator" data-column="domain">Domain</div>
<div class="stats-grid-header text-right sort-indicator" data-column="count">Requests</div>
<div class="stats-grid-header text-right sort-indicator" data-column="bytes">Transferred</div>
</div>
<% @by_domain_counts
.sort_by { |_ignore, stats| -stats[:bytes] }
.each do |domain, stats| %>
<div class="grid-row contents" data-domain="<%= domain %>" data-count="<%= stats[:count] %>" data-bytes="<%= stats[:bytes] %>">
<div class="grid-cell font-medium text-slate-900 text-sm"><%= domain %></div>
<div class="grid-cell text-right text-slate-600 text-sm"><%= stats[:count] %></div>
<div class="grid-cell text-right text-slate-600 font-medium text-sm">
<%= HexUtil.humansize(stats[:bytes]) %>
</div>
</div>
<% end %>
</div>
</div>
</div> </div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize sortable tables
document.querySelectorAll('.sortable-table').forEach(table => {
initSortableTable(table);
});
function initSortableTable(table) {
const headers = table.querySelectorAll('.stats-grid-header');
headers.forEach(header => {
header.addEventListener('click', function() {
const column = this.getAttribute('data-column');
const isAsc = !this.classList.contains('asc');
// Reset all headers
headers.forEach(h => h.classList.remove('asc'));
// Set current header state
if (isAsc) {
this.classList.add('asc');
}
// Get all rows except the header row
const rows = Array.from(table.querySelectorAll('.grid-row:not(:first-child)'));
// Sort the rows
rows.sort((a, b) => {
let aValue, bValue;
if (table.id === 'content-type-table') {
if (column === 'content_type') {
aValue = a.getAttribute('data-content-type');
bValue = b.getAttribute('data-content-type');
return isAsc ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue);
} else {
aValue = parseInt(a.getAttribute(`data-${column}`), 10);
bValue = parseInt(b.getAttribute(`data-${column}`), 10);
}
} else if (table.id === 'domain-table') {
if (column === 'domain') {
aValue = a.getAttribute('data-domain');
bValue = b.getAttribute('data-domain');
return isAsc ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue);
} else {
aValue = parseInt(a.getAttribute(`data-${column}`), 10);
bValue = parseInt(b.getAttribute(`data-${column}`), 10);
}
}
return isAsc ? aValue - bValue : bValue - aValue;
});
// Remove existing rows
rows.forEach(row => row.remove());
// Append sorted rows
rows.forEach(row => {
table.appendChild(row);
});
});
});
}
});
</script>

View File

@@ -10,7 +10,9 @@ START_PROMETHEUS_EXPORTER =
# always start in sever mode # always start in sever mode
Rails.const_defined?("Server") || Rails.const_defined?("Server") ||
# always start in worker mode # always start in worker mode
(Rails.env == "worker"), (Rails.env == "worker") ||
# ran with args when in server mode, but no top level task
(ARGV.any? && Rake.application.top_level_tasks.empty?),
T::Boolean, T::Boolean,
) )

View File

@@ -2,6 +2,7 @@
require "rails_helper" require "rails_helper"
RSpec.describe LogEntriesController, type: :controller do RSpec.describe LogEntriesController, type: :controller do
render_views
let(:user) { create(:user, :admin) } let(:user) { create(:user, :admin) }
context "when user is not signed in" do context "when user is not signed in" do
@@ -18,24 +19,59 @@ RSpec.describe LogEntriesController, type: :controller do
expect(response).to redirect_to(new_user_session_path) expect(response).to redirect_to(new_user_session_path)
end end
end end
describe "GET #show" do
it "redirects to sign in" do
get :show, params: { id: 1 }
expect(response).to redirect_to(new_user_session_path)
end
end
end end
context "when user is signed in" do context "when user is signed in" do
before { sign_in user } before { sign_in user }
describe "GET #index" do describe "GET #index" do
it "returns filtered log entries" do it "renders the index template with filtered log entries" do
get :index, params: { filter: "example.com/test" } get :index, params: { filter: "example.com/test" }
expect(response).to be_successful expect(response).to be_successful
expect(response).to render_template(:index)
end
it "shows existing log entries matching the filter" do
log_entry =
create(
:http_log_entry,
uri_host: "example.com",
uri_path: "/test",
uri_scheme: "https",
)
get :index, params: { filter: "example.com/test" }
expect(response).to be_successful
expect(assigns(:log_entries)).to include(log_entry)
end end
end end
describe "GET #stats" do describe "GET #stats" do
it "returns statistics in the specified time window" do it "renders the stats template with time window data" do
get :stats, params: { seconds: 3600 } get :stats, params: { seconds: 3600 }
expect(response).to be_successful expect(response).to be_successful
expect(response).to render_template(:stats)
expect(assigns(:time_window)).to eq 3600.seconds expect(assigns(:time_window)).to eq 3600.seconds
end end
end end
describe "GET #show" do
let(:log_entry) { create(:http_log_entry) }
it "renders the show template with the requested log entry" do
get :show, params: { id: log_entry.id }
expect(response).to be_successful
expect(response).to render_template(:show)
expect(assigns(:log_entry)).to eq(log_entry)
end
end
end end
end end

View File

View File