refactor user nav bar and log entry index

This commit is contained in:
Dylan Knutson
2025-07-30 06:59:34 +00:00
parent 8bcdd9b451
commit 4e94a8911c
9 changed files with 352 additions and 243 deletions

View File

@@ -1,144 +0,0 @@
import * as React from 'react';
import { useRef, useEffect, useState } from 'react';
interface UserMenuProps {
userEmail: string;
userRole?: 'admin' | 'moderator';
editProfilePath: string;
signOutPath: string;
csrfToken: string;
globalStatesPath: string;
goodJobPath: string;
grafanaPath: string;
prometheusPath: string;
}
export const UserMenu: React.FC<UserMenuProps> = ({
userEmail,
userRole,
editProfilePath,
signOutPath,
csrfToken,
globalStatesPath,
goodJobPath,
grafanaPath,
prometheusPath,
}) => {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleSignOut = (e: React.FormEvent) => {
e.preventDefault();
const form = document.createElement('form');
form.method = 'POST';
form.action = signOutPath;
form.style.display = 'none';
const methodInput = document.createElement('input');
methodInput.type = 'hidden';
methodInput.name = '_method';
methodInput.value = 'delete';
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'authenticity_token';
csrfInput.value = csrfToken;
form.appendChild(methodInput);
form.appendChild(csrfInput);
document.body.appendChild(form);
form.submit();
};
return (
<div className="relative" ref={menuRef}>
<button
className="flex items-center space-x-2 text-slate-600 hover:text-slate-900 focus:outline-none"
onClick={() => setIsOpen(!isOpen)}
>
<i className="fas fa-user-circle text-2xl" />
<i className="fas fa-chevron-down text-xs" />
</button>
<div
className={`absolute right-0 z-50 mt-2 w-48 rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-200 ${
isOpen ? 'visible opacity-100' : 'invisible opacity-0'
}`}
>
<div className="border-b border-slate-200 px-4 py-2 text-sm text-slate-700">
<div className="font-medium">{userEmail}</div>
{userRole === 'admin' && (
<span className="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800">
Admin
</span>
)}
{userRole === 'moderator' && (
<span className="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800">
Mod
</span>
)}
</div>
{userRole === 'admin' && (
<>
<a
href={globalStatesPath}
className="flex w-full items-center px-4 py-2 text-sm text-slate-700 hover:bg-slate-100"
>
<i className="fas fa-cogs mr-2 w-5" />
<span>Global State</span>
</a>
<a
href={goodJobPath}
className="flex w-full items-center px-4 py-2 text-sm text-slate-700 hover:bg-slate-100"
>
<i className="fas fa-tasks mr-2 w-5" />
<span>Jobs Queue</span>
</a>
<a
href={grafanaPath}
className="flex w-full items-center px-4 py-2 text-sm text-slate-700 hover:bg-slate-100"
>
<i className="fas fa-chart-line mr-2 w-5" />
<span>Grafana</span>
</a>
<a
href={prometheusPath}
className="flex w-full items-center px-4 py-2 text-sm text-slate-700 hover:bg-slate-100"
>
<i className="fas fa-chart-bar mr-2 w-5" />
<span>Prometheus</span>
</a>
</>
)}
<a
href={editProfilePath}
className="flex w-full items-center px-4 py-2 text-sm text-slate-700 hover:bg-slate-100"
>
<i className="fas fa-cog mr-2 w-5" />
<span>Edit Profile</span>
</a>
<button
onClick={handleSignOut}
className="flex w-full items-center px-4 py-2 text-left text-sm text-slate-700 hover:bg-slate-100"
>
<i className="fas fa-sign-out-alt mr-2 w-5" />
<span>Sign Out</span>
</button>
</div>
</div>
);
};

View File

@@ -296,7 +296,7 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
/> />
</label> </label>
{anyShown && ( {anyShown && (
<div className="absolute left-0 right-0 top-full z-50 mt-1"> <div className="absolute left-0 right-0 top-full mt-1">
<UserSearchBarItems /> <UserSearchBarItems />
</div> </div>
)} )}

View File

@@ -0,0 +1,165 @@
/**
* User Menu Handler
* Manages the responsive user menu that works for both desktop and mobile layouts
*/
interface UserMenuElements {
button: HTMLButtonElement;
menu: HTMLDivElement;
menuIcon: SVGElement | null;
closeIcon: SVGElement | null;
chevronIcon: HTMLElement | null;
}
class UserMenu {
private elements: UserMenuElements | null = null;
constructor() {
this.initialize();
}
private initialize(): void {
const userMenuButton = document.querySelector(
'#user-menu-button',
) as HTMLButtonElement;
const userMenu = document.querySelector('#user-menu') as HTMLDivElement;
const menuIcon = document.querySelector('.menu-icon') as SVGElement | null;
const closeIcon = document.querySelector(
'.close-icon',
) as SVGElement | null;
const chevronIcon = document.querySelector(
'.chevron-icon',
) as HTMLElement | null;
if (!userMenuButton || !userMenu) {
return; // User menu not present (e.g., guest user)
}
this.elements = {
button: userMenuButton,
menu: userMenu,
menuIcon,
closeIcon,
chevronIcon,
};
this.attachEventListeners();
}
private attachEventListeners(): void {
if (!this.elements) return;
// Toggle menu on button click
this.elements.button.addEventListener(
'click',
this.handleMenuToggle.bind(this),
);
// Close menu when clicking outside
document.addEventListener('click', this.handleClickOutside.bind(this));
// Close menu on escape key
document.addEventListener('keydown', this.handleEscapeKey.bind(this));
}
private handleMenuToggle(): void {
if (!this.elements) return;
const isOpen = !this.elements.menu.classList.contains('hidden');
if (isOpen) {
this.closeMenu();
} else {
this.openMenu();
}
}
private handleClickOutside(event: MouseEvent): void {
if (!this.elements) return;
const target = event.target as Node;
if (
!this.elements.button.contains(target) &&
!this.elements.menu.contains(target)
) {
this.closeMenu();
}
}
private handleEscapeKey(event: KeyboardEvent): void {
if (!this.elements) return;
if (
event.key === 'Escape' &&
!this.elements.menu.classList.contains('hidden')
) {
this.closeMenu();
}
}
private openMenu(): void {
if (!this.elements) return;
// Show menu but keep it in initial animation state
this.elements.menu.classList.remove('hidden');
this.elements.button.setAttribute('aria-expanded', 'true');
// Ensure initial state is set
this.elements.menu.classList.add('opacity-0', '-translate-y-2');
this.elements.menu.classList.remove('opacity-100', 'translate-y-0');
// Force a reflow to ensure the initial state is applied
this.elements.menu.offsetHeight;
// Trigger animation by transitioning to visible state
requestAnimationFrame(() => {
this.elements!.menu.classList.remove('opacity-0', '-translate-y-2');
this.elements!.menu.classList.add('opacity-100', 'translate-y-0');
});
// Update mobile icons (hamburger -> close)
if (this.elements.menuIcon && this.elements.closeIcon) {
this.elements.menuIcon.classList.add('hidden');
this.elements.closeIcon.classList.remove('hidden');
}
// Update desktop chevron (rotate down)
if (this.elements.chevronIcon) {
this.elements.chevronIcon.classList.add('rotate-180');
}
}
private closeMenu(): void {
if (!this.elements) return;
// Animate out: fade and slide up
this.elements.menu.classList.remove('opacity-100', 'translate-y-0');
this.elements.menu.classList.add('opacity-0', '-translate-y-2');
// Hide menu after CSS animation completes
setTimeout(() => {
if (this.elements) {
this.elements.menu.classList.add('hidden');
}
}, 150); // Match the CSS transition duration
this.elements.button.setAttribute('aria-expanded', 'false');
// Update mobile icons (close -> hamburger)
if (this.elements.menuIcon && this.elements.closeIcon) {
this.elements.menuIcon.classList.remove('hidden');
this.elements.closeIcon.classList.add('hidden');
}
// Update desktop chevron (rotate back up)
if (this.elements.chevronIcon) {
this.elements.chevronIcon.classList.remove('rotate-180');
}
}
}
// Initialize user menu when DOM is loaded
export function initUserMenu(): void {
new UserMenu();
}

View File

@@ -1,18 +1,17 @@
import ReactOnRails from 'react-on-rails'; import ReactOnRails from 'react-on-rails';
import UserSearchBar from '../bundles/Main/components/UserSearchBar'; import UserSearchBar from '../bundles/Main/components/UserSearchBar';
import { UserMenu } from '../bundles/Main/components/UserMenu';
import { PostHoverPreviewWrapper } from '../bundles/Main/components/PostHoverPreviewWrapper'; import { PostHoverPreviewWrapper } from '../bundles/Main/components/PostHoverPreviewWrapper';
import { UserHoverPreviewWrapper } from '../bundles/Main/components/UserHoverPreviewWrapper'; import { UserHoverPreviewWrapper } from '../bundles/Main/components/UserHoverPreviewWrapper';
import { TrackedObjectsChart } from '../bundles/Main/components/TrackedObjectsChart'; import { TrackedObjectsChart } from '../bundles/Main/components/TrackedObjectsChart';
import { initCollapsibleSections } from '../bundles/UI/collapsibleSections'; import { initCollapsibleSections } from '../bundles/UI/collapsibleSections';
import { IpAddressInput } from '../bundles/UI/components'; import { IpAddressInput } from '../bundles/UI/components';
import { StatsPage } from '../bundles/Main/components/StatsPage'; import { StatsPage } from '../bundles/Main/components/StatsPage';
import { initUserMenu } from '../bundles/UI/userMenu';
// This is how react_on_rails can see the components in the browser. // This is how react_on_rails can see the components in the browser.
ReactOnRails.register({ ReactOnRails.register({
UserSearchBar, UserSearchBar,
UserMenu,
PostHoverPreviewWrapper, PostHoverPreviewWrapper,
UserHoverPreviewWrapper, UserHoverPreviewWrapper,
TrackedObjectsChart, TrackedObjectsChart,
@@ -20,7 +19,8 @@ ReactOnRails.register({
StatsPage, StatsPage,
}); });
// Initialize collapsible sections // Initialize UI components
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
initCollapsibleSections(); initCollapsibleSections();
initUserMenu();
}); });

View File

@@ -1,13 +1,11 @@
import ReactOnRails from 'react-on-rails'; import ReactOnRails from 'react-on-rails';
import UserSearchBar from '../bundles/Main/components/UserSearchBarServer'; import UserSearchBar from '../bundles/Main/components/UserSearchBarServer';
import { UserMenu } from '../bundles/Main/components/UserMenu';
import { PostHoverPreviewWrapper } from '../bundles/Main/components/PostHoverPreviewWrapper'; import { PostHoverPreviewWrapper } from '../bundles/Main/components/PostHoverPreviewWrapper';
import { UserHoverPreviewWrapper } from '../bundles/Main/components/UserHoverPreviewWrapper'; import { UserHoverPreviewWrapper } from '../bundles/Main/components/UserHoverPreviewWrapper';
// This is how react_on_rails can see the UserSearchBar in the browser. // This is how react_on_rails can see the UserSearchBar in the browser.
ReactOnRails.register({ ReactOnRails.register({
UserMenu,
UserSearchBar, UserSearchBar,
PostHoverPreviewWrapper, PostHoverPreviewWrapper,
UserHoverPreviewWrapper, UserHoverPreviewWrapper,

View File

@@ -22,72 +22,154 @@
<%= yield :head %> <%= yield :head %>
</head> </head>
<body class="mx-0 flex flex-col h-full"> <body class="mx-0 flex flex-col h-full">
<header class="bg-slate-100 border-slate-200 border-b-2"> <header class="bg-white border-b border-slate-200 shadow-sm top-0">
<div class="mx-auto max-w-5xl py-6 px-6 sm:px-8 flex items-baseline"> <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<span class="flex flex-col gap-2"> <div class="flex items-center justify-between h-16">
<h1 class="text-4xl sm:text-5xl font-bold text-slate-900"> <!-- Logo and Brand -->
<%= link_to root_path, class: "flex items-center" do %> <div class="flex items-center">
<%= image_tag asset_path("refurrer-logo-md.png"), class: "w-12 h-12 mr-2" %> <%= link_to root_path, class: "flex items-center space-x-2 hover:opacity-80 transition-opacity" do %>
<span class="text-4xl sm:text-5xl font-bold text-slate-900"> <%= image_tag asset_path("refurrer-logo-md.png"), class: "w-8 h-8 sm:w-10 sm:h-10", alt: "ReFurrer Logo" %>
ReFurrer <div class="flex flex-col">
</span> <span class="text-lg sm:text-xl font-bold text-slate-900 leading-tight">
ReFurrer
</span>
<span class="text-xs text-slate-500 font-medium hidden sm:block leading-tight">
Furry Swiss Army Knife
</span>
</div>
<% end %> <% end %>
</h1> </div>
<% if policy(IpAddressRole).view_debug_info? %> <!-- User Menu (works for both desktop and mobile) -->
<span class="font-mono text-slate-500"> <div class="relative">
<span>IP address</span> <% if user_signed_in? %>
<span class="font-mono text-slate-700"> <button type="button"
<%= request.remote_ip %> id="user-menu-button"
</span> class="flex items-center space-x-2 rounded-md px-3 py-2 text-slate-600 transition-colors hover:bg-slate-50 hover:text-slate-900 touch-manipulation"
</span> aria-controls="user-menu"
<span class="font-mono text-slate-500"> aria-expanded="false"
<span> style="touch-action: manipulation; -webkit-touch-callout: none;">
<%= link_to "IP Address Role", state_ip_address_roles_path, class: "text-blue-500 hover:text-blue-700" %> <!-- User Avatar -->
</span> <div class="flex h-8 w-8 items-center justify-center rounded-full bg-slate-600 text-sm font-medium text-white">
<span class="font-mono text-slate-700 relative group"> <%= current_user.email.first.upcase %>
<% if role = current_ip_address_role %> </div>
<%= role.ip_address %> / <%= role.role %> <!-- Mobile: Show hamburger icon -->
<% if role.description.present? %> <div class="sm:hidden">
<div class="absolute hidden group-hover:block bg-slate-800 text-white text-sm rounded px-2 py-1 top-full left-0 mt-1 whitespace-nowrap z-10"> <svg class="menu-icon block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<%= role.description %> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
<svg class="close-icon hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<!-- Desktop: Show chevron -->
<i class="chevron-icon hidden sm:block fas fa-chevron-down text-xs transition-transform"></i>
</button>
<!-- User Menu Dropdown -->
<div class="hidden fixed sm:absolute left-0 right-0 sm:left-auto z-10 sm:mt-4 sm:w-64 sm:rounded-lg bg-white py-2 shadow-lg border border-slate-200 opacity-0 -translate-y-2 transition-all duration-150 ease-out"
id="user-menu">
<!-- User Info Header -->
<div class="border-b border-slate-200 px-4 py-3 sm:py-3 max-sm:py-4">
<div class="flex items-center space-x-3">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-slate-600 text-lg font-medium text-white">
<%= current_user.email.first.upcase %>
</div> </div>
<% end %> <div class="min-w-0 flex-1">
<% else %> <div class="truncate text-sm font-medium text-slate-900">
None <%= current_user.email %>
</div>
<% if current_user.role == 'admin' %>
<span class="mt-1 inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium bg-red-100 text-red-800">
Administrator
</span>
<% elsif current_user.role == 'moderator' %>
<span class="mt-1 inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium bg-blue-100 text-blue-800">
Moderator
</span>
<% end %>
</div>
</div>
</div>
<!-- Admin Tools -->
<% if current_user.role == 'admin' %>
<div class="py-2">
<div class="px-4 py-2">
<div class="text-xs font-semibold uppercase tracking-wider text-slate-500">
Admin Tools
</div>
</div>
<%= link_to global_states_path, class: "flex w-full items-center px-4 py-2 text-sm text-slate-700 transition-colors hover:bg-slate-50 hover:text-slate-900" do %>
<i class="fas fa-cogs mr-3 w-4 text-slate-400"></i>
<span>Global State</span>
<% end %>
<%= link_to good_job_path, class: "flex w-full items-center px-4 py-2 text-sm text-slate-700 transition-colors hover:bg-slate-50 hover:text-slate-900" do %>
<i class="fas fa-tasks mr-3 w-4 text-slate-400"></i>
<span>Jobs Queue</span>
<% end %>
<%= link_to grafana_path, class: "flex w-full items-center px-4 py-2 text-sm text-slate-700 transition-colors hover:bg-slate-50 hover:text-slate-900" do %>
<i class="fas fa-chart-line mr-3 w-4 text-slate-400"></i>
<span>Grafana</span>
<% end %>
<%= link_to prometheus_path, class: "flex w-full items-center px-4 py-2 text-sm text-slate-700 transition-colors hover:bg-slate-50 hover:text-slate-900" do %>
<i class="fas fa-chart-bar mr-3 w-4 text-slate-400"></i>
<span>Prometheus</span>
<% end %>
<div class="my-2 border-t border-slate-200"></div>
</div>
<% end %> <% end %>
</span> <!-- User Options -->
</span> <div class="py-2">
<% end %> <%= link_to edit_user_registration_path, class: "flex w-full items-center px-4 py-2 text-sm text-slate-700 transition-colors hover:bg-slate-50 hover:text-slate-900" do %>
</span> <i class="fas fa-user-cog mr-3 w-4 text-slate-400"></i>
<div class="flex-grow"></div> <span>Edit Profile</span>
<nav class="flex items-center space-x-4"> <% end %>
<% if user_signed_in? %> <%= link_to destroy_user_session_path, method: :delete, class: "flex w-full items-center px-4 py-2 text-sm text-slate-700 transition-colors hover:bg-slate-50 hover:text-slate-900" do %>
<%= react_component("UserMenu", prerender: true, strict_mode: true, props: { <i class="fas fa-sign-out-alt mr-3 w-4 text-slate-400"></i>
userEmail: current_user.email, <span>Sign Out</span>
userRole: current_user.role, <% end %>
editProfilePath: edit_user_registration_path, </div>
signOutPath: destroy_user_session_path, </div>
csrfToken: form_authenticity_token, <% else %>
globalStatesPath: global_states_path, <!-- Guest Navigation -->
goodJobPath: good_job_path, <div class="flex items-center space-x-2">
grafanaPath: grafana_path, <%= link_to new_user_session_path, class: "inline-flex items-center px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 hover:bg-slate-50 rounded-md transition-colors" do %>
prometheusPath: prometheus_path <i class="fas fa-sign-in-alt mr-2"></i>
}) %> <span class="hidden sm:inline">Sign In</span>
<% else %> <span class="sm:hidden">Login</span>
<%= link_to new_user_session_path, class: "text-slate-600 hover:text-slate-900" do %> <% end %>
<i class="fas fa-sign-in-alt mr-1"></i> <%= link_to new_user_registration_path, class: "inline-flex items-center px-3 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors" do %>
Sign In <i class="fas fa-user-plus mr-2"></i>
<span class="hidden sm:inline">Sign Up</span>
<span class="sm:hidden">Join</span>
<% end %>
</div>
<% end %> <% end %>
<%= link_to new_user_registration_path, class: "text-slate-600 hover:text-slate-900" do %> </div>
<i class="fas fa-user-plus mr-1"></i> </div>
Sign Up
<% end %>
<% end %>
</nav>
<h2 class="text-1xl sm:text-2xl italic font-bold text-slate-500 ml-4">
Furry Swiss Army Knife
</h2>
</div> </div>
<!-- Debug Info Bar (shown only when applicable and in condensed form on mobile) -->
<% if policy(IpAddressRole).view_debug_info? %>
<div class="bg-slate-50 border-t border-slate-200 px-4 py-2">
<div class="mx-auto max-w-7xl">
<div class="flex flex-col sm:flex-row text-xs text-slate-600 gap-1 sm:gap-4">
<div class="flex">
<span class="font-medium">IP:</span>
<span class="font-mono truncate">2001:0db8:85a3:0000:0000:8a2e:0370:7334</span>
</div>
<div class="flex">
<%= link_to "Role", state_ip_address_roles_path, class: "text-blue-600 hover:text-blue-800 font-medium" %>
<span class="font-medium">:</span>
<span class="font-mono">
<% if role = current_ip_address_role %>
<%= role.ip_address %> / <%= role.role %>
<% else %>
None
<% end %>
</span>
</div>
</div>
</div>
</div>
<% end %>
</header> </header>
<main class="flex flex-col grow bg-slate-200"> <main class="flex flex-col grow bg-slate-200">
<% if notice %> <% if notice %>

View File

@@ -36,8 +36,8 @@
<% 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_auto_1fr_auto_auto_auto] max-w-screen-lg mx-auto border border-slate-200 divide-y divide-slate-200 bg-white shadow mb-4 rounded-lg"> <div class="flex flex-col gap-2 sm:gap-0 sm:grid sm:grid-cols-[auto_auto_auto_auto_auto_1fr_auto_auto_auto] max-w-full sm:max-w-screen-lg mx-auto border border-slate-200 sm:divide-y sm:divide-slate-200 sm:bg-white shadow mb-4 rounded-lg">
<div class="grid grid-cols-subgrid col-span-full px-2"> <div class="hidden sm:grid sm:grid-cols-subgrid sm:col-span-full px-2">
<div class="log-entry-table-header-cell text-center rounded-tl col-span-2">ID</div> <div class="log-entry-table-header-cell text-center rounded-tl col-span-2">ID</div>
<div class="log-entry-table-header-cell text-right">Size</div> <div class="log-entry-table-header-cell text-right">Size</div>
<div class="log-entry-table-header-cell text-center">Time</div> <div class="log-entry-table-header-cell text-center">Time</div>
@@ -48,25 +48,29 @@
<div class="log-entry-table-header-cell text-right rounded-tr">Resp</div> <div class="log-entry-table-header-cell text-right rounded-tr">Resp</div>
</div> </div>
<% @log_entries.each do |hle| %> <% @log_entries.each do |hle| %>
<div class="grid grid-cols-subgrid col-span-full group divide-x [&>*]:pl-2 [&>*]:pr-2 divide-slate-200 hover:bg-slate-50"> <div class="flex flex-col px-2 truncate bg-white sm:bg-transparent sm:grid sm:grid-cols-subgrid sm:col-span-full group sm:[&>*]:pr-2 hover:bg-slate-50">
<div class="log-entry-table-row-cell text-center text-slate-400"> <div class="flex flex-row gap-2 justify-between sm:contents sm:[&>*]:pr-2">
<span class="text-sm font-medium" title="<%= hle.performed_by %>"><%= performed_by_to_short_code(hle.performed_by) %></span> <div class="log-entry-table-row-cell sm:text-center text-slate-400">
<span class="text-sm font-medium" title="<%= hle.performed_by %>"><%= performed_by_to_short_code(hle.performed_by) %></span>
</div>
<div class="log-entry-table-row-cell sm:justify-end">
<%= link_to hle.id, log_entry_path(hle.id), class: "text-blue-600 hover:text-blue-800 font-medium" %>
</div>
<div class="log-entry-table-row-cell sm:justify-end">
<%= HexUtil.humansize(hle.response_size) %>
</div>
</div> </div>
<div class="log-entry-table-row-cell justify-end"> <div class="flex flex-row gap-2 justify-between sm:contents sm:[&>*]:pr-2">
<%= link_to hle.id, log_entry_path(hle.id), class: "text-blue-600 hover:text-blue-800 font-medium" %> <div class="log-entry-table-row-cell sm:text-right">
<%= time_ago_in_words(hle.created_at, include_seconds: true) %>
</div>
<div class="log-entry-table-row-cell sm:justify-center">
<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 %>
</span>
</div>
</div> </div>
<div class="log-entry-table-row-cell justify-end"> <div class="log-entry-table-row-cell sm:text-right sm:min-w-0">
<%= HexUtil.humansize(hle.response_size) %>
</div>
<div class="log-entry-table-row-cell text-right">
<%= time_ago_in_words(hle.created_at, include_seconds: true) %>
</div>
<div class="log-entry-table-row-cell justify-center">
<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 %>
</span>
</div>
<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 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"> <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="py-1 bg-slate-100 rounded-l hover:bg-slate-200 transition-colors" 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 %>">
@@ -88,18 +92,20 @@
<%- end -%> <%- end -%>
</div> </div>
</div> </div>
<div class="log-entry-table-row-cell text-center"> <div class="flex flex-row gap-2 justify-between sm:contents sm:[&>*]:pr-2">
<%= link_to hle.uri.to_s, class: "text-blue-600 hover:text-blue-800 transition-colors", target: "_blank", rel: "noreferrer" do %> <div class="log-entry-table-row-cell sm:text-center">
<%= render partial: "shared/icons/external_link", locals: { class_name: "w-4 h-4" } %> <%= link_to hle.uri.to_s, class: "text-blue-600 hover:text-blue-800 transition-colors", target: "_blank", rel: "noreferrer" do %>
<% end %> <%= render partial: "shared/icons/external_link", locals: { class_name: "w-4 h-4" } %>
</div> <% end %>
<div class="log-entry-table-row-cell"> </div>
<span class="max-w-24 truncate inline-block" title="<%= hle.content_type %>"> <div class="log-entry-table-row-cell">
<%= hle.content_type.split(";")[0] %> <span class="max-w-24 truncate inline-block" title="<%= hle.content_type %>">
</span> <%= hle.content_type.split(";")[0] %>
</div> </span>
<div class="justify-end log-entry-table-row-cell"> </div>
<%= hle.response_time_ms %>ms <div class="justify-end log-entry-table-row-cell">
<%= hle.response_time_ms %>ms
</div>
</div> </div>
</div> </div>
<% end %> <% end %>

View File

@@ -3,7 +3,7 @@ if Rails.env.development? || Rails.env.staging?
# Rack::MiniProfiler.config.pre_authorize_cb = lambda { |env| true } # Rack::MiniProfiler.config.pre_authorize_cb = lambda { |env| true }
Rack::MiniProfiler.config.authorization_mode = :allow_all Rack::MiniProfiler.config.authorization_mode = :allow_all
Rack::MiniProfiler.config.enable_advanced_debugging_tools = true Rack::MiniProfiler.config.enable_advanced_debugging_tools = true
Rack::MiniProfiler.config.position = "top-right" Rack::MiniProfiler.config.position = "top-left"
Rack::MiniProfiler.config.disable_caching = false Rack::MiniProfiler.config.disable_caching = false
Rack::MiniProfiler.config.skip_paths = [%r{/blobs/.+/contents.jpg$}] Rack::MiniProfiler.config.skip_paths = [%r{/blobs/.+/contents.jpg$}]
end end

View File

@@ -25,7 +25,9 @@ module.exports = {
experimental: { experimental: {
classRegex: [ classRegex: [
/\\bclass:\s*'([^']*)'/, /\\bclass:\s*'([^']*)'/,
/\\bclass=\s*'([^']*)'/,
/\\bclass:\s*"([^"]*)"/, /\\bclass:\s*"([^"]*)"/,
/\\bclass=\s*"([^"]*)"/,
/["'`]([^"'`]*).*?,?\s?/, /["'`]([^"'`]*).*?,?\s?/,
], ],
}, },