refactor user nav bar and log entry index
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -296,7 +296,7 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
|
||||
/>
|
||||
</label>
|
||||
{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 />
|
||||
</div>
|
||||
)}
|
||||
|
||||
165
app/javascript/bundles/UI/userMenu.ts
Normal file
165
app/javascript/bundles/UI/userMenu.ts
Normal 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();
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
import ReactOnRails from 'react-on-rails';
|
||||
|
||||
import UserSearchBar from '../bundles/Main/components/UserSearchBar';
|
||||
import { UserMenu } from '../bundles/Main/components/UserMenu';
|
||||
import { PostHoverPreviewWrapper } from '../bundles/Main/components/PostHoverPreviewWrapper';
|
||||
import { UserHoverPreviewWrapper } from '../bundles/Main/components/UserHoverPreviewWrapper';
|
||||
import { TrackedObjectsChart } from '../bundles/Main/components/TrackedObjectsChart';
|
||||
import { initCollapsibleSections } from '../bundles/UI/collapsibleSections';
|
||||
import { IpAddressInput } from '../bundles/UI/components';
|
||||
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.
|
||||
ReactOnRails.register({
|
||||
UserSearchBar,
|
||||
UserMenu,
|
||||
PostHoverPreviewWrapper,
|
||||
UserHoverPreviewWrapper,
|
||||
TrackedObjectsChart,
|
||||
@@ -20,7 +19,8 @@ ReactOnRails.register({
|
||||
StatsPage,
|
||||
});
|
||||
|
||||
// Initialize collapsible sections
|
||||
// Initialize UI components
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initCollapsibleSections();
|
||||
initUserMenu();
|
||||
});
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import ReactOnRails from 'react-on-rails';
|
||||
|
||||
import UserSearchBar from '../bundles/Main/components/UserSearchBarServer';
|
||||
import { UserMenu } from '../bundles/Main/components/UserMenu';
|
||||
import { PostHoverPreviewWrapper } from '../bundles/Main/components/PostHoverPreviewWrapper';
|
||||
import { UserHoverPreviewWrapper } from '../bundles/Main/components/UserHoverPreviewWrapper';
|
||||
|
||||
// This is how react_on_rails can see the UserSearchBar in the browser.
|
||||
ReactOnRails.register({
|
||||
UserMenu,
|
||||
UserSearchBar,
|
||||
PostHoverPreviewWrapper,
|
||||
UserHoverPreviewWrapper,
|
||||
|
||||
@@ -22,72 +22,154 @@
|
||||
<%= yield :head %>
|
||||
</head>
|
||||
<body class="mx-0 flex flex-col h-full">
|
||||
<header class="bg-slate-100 border-slate-200 border-b-2">
|
||||
<div class="mx-auto max-w-5xl py-6 px-6 sm:px-8 flex items-baseline">
|
||||
<span class="flex flex-col gap-2">
|
||||
<h1 class="text-4xl sm:text-5xl font-bold text-slate-900">
|
||||
<%= link_to root_path, class: "flex items-center" do %>
|
||||
<%= image_tag asset_path("refurrer-logo-md.png"), class: "w-12 h-12 mr-2" %>
|
||||
<span class="text-4xl sm:text-5xl font-bold text-slate-900">
|
||||
ReFurrer
|
||||
</span>
|
||||
<header class="bg-white border-b border-slate-200 shadow-sm top-0">
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Logo and Brand -->
|
||||
<div class="flex items-center">
|
||||
<%= link_to root_path, class: "flex items-center space-x-2 hover:opacity-80 transition-opacity" do %>
|
||||
<%= image_tag asset_path("refurrer-logo-md.png"), class: "w-8 h-8 sm:w-10 sm:h-10", alt: "ReFurrer Logo" %>
|
||||
<div class="flex flex-col">
|
||||
<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 %>
|
||||
</h1>
|
||||
<% if policy(IpAddressRole).view_debug_info? %>
|
||||
<span class="font-mono text-slate-500">
|
||||
<span>IP address</span>
|
||||
<span class="font-mono text-slate-700">
|
||||
<%= request.remote_ip %>
|
||||
</span>
|
||||
</span>
|
||||
<span class="font-mono text-slate-500">
|
||||
<span>
|
||||
<%= link_to "IP Address Role", state_ip_address_roles_path, class: "text-blue-500 hover:text-blue-700" %>
|
||||
</span>
|
||||
<span class="font-mono text-slate-700 relative group">
|
||||
<% if role = current_ip_address_role %>
|
||||
<%= role.ip_address %> / <%= role.role %>
|
||||
<% if role.description.present? %>
|
||||
<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">
|
||||
<%= role.description %>
|
||||
</div>
|
||||
<!-- User Menu (works for both desktop and mobile) -->
|
||||
<div class="relative">
|
||||
<% if user_signed_in? %>
|
||||
<button type="button"
|
||||
id="user-menu-button"
|
||||
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"
|
||||
aria-controls="user-menu"
|
||||
aria-expanded="false"
|
||||
style="touch-action: manipulation; -webkit-touch-callout: none;">
|
||||
<!-- User Avatar -->
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-slate-600 text-sm font-medium text-white">
|
||||
<%= current_user.email.first.upcase %>
|
||||
</div>
|
||||
<!-- Mobile: Show hamburger icon -->
|
||||
<div class="sm:hidden">
|
||||
<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">
|
||||
<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>
|
||||
<% end %>
|
||||
<% else %>
|
||||
None
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm font-medium text-slate-900">
|
||||
<%= 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 %>
|
||||
</span>
|
||||
</span>
|
||||
<% end %>
|
||||
</span>
|
||||
<div class="flex-grow"></div>
|
||||
<nav class="flex items-center space-x-4">
|
||||
<% if user_signed_in? %>
|
||||
<%= react_component("UserMenu", prerender: true, strict_mode: true, props: {
|
||||
userEmail: current_user.email,
|
||||
userRole: current_user.role,
|
||||
editProfilePath: edit_user_registration_path,
|
||||
signOutPath: destroy_user_session_path,
|
||||
csrfToken: form_authenticity_token,
|
||||
globalStatesPath: global_states_path,
|
||||
goodJobPath: good_job_path,
|
||||
grafanaPath: grafana_path,
|
||||
prometheusPath: prometheus_path
|
||||
}) %>
|
||||
<% else %>
|
||||
<%= link_to new_user_session_path, class: "text-slate-600 hover:text-slate-900" do %>
|
||||
<i class="fas fa-sign-in-alt mr-1"></i>
|
||||
Sign In
|
||||
<!-- User Options -->
|
||||
<div class="py-2">
|
||||
<%= 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 %>
|
||||
<i class="fas fa-user-cog mr-3 w-4 text-slate-400"></i>
|
||||
<span>Edit Profile</span>
|
||||
<% end %>
|
||||
<%= 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 %>
|
||||
<i class="fas fa-sign-out-alt mr-3 w-4 text-slate-400"></i>
|
||||
<span>Sign Out</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<!-- Guest Navigation -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<%= 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 %>
|
||||
<i class="fas fa-sign-in-alt mr-2"></i>
|
||||
<span class="hidden sm:inline">Sign In</span>
|
||||
<span class="sm:hidden">Login</span>
|
||||
<% end %>
|
||||
<%= 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 %>
|
||||
<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 %>
|
||||
<%= link_to new_user_registration_path, class: "text-slate-600 hover:text-slate-900" do %>
|
||||
<i class="fas fa-user-plus mr-1"></i>
|
||||
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>
|
||||
</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>
|
||||
<main class="flex flex-col grow bg-slate-200">
|
||||
<% if notice %>
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<%= 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="grid grid-cols-subgrid col-span-full px-2">
|
||||
<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="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-right">Size</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>
|
||||
<% @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="log-entry-table-row-cell 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 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="flex flex-row gap-2 justify-between sm:contents sm:[&>*]:pr-2">
|
||||
<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 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 font-medium" %>
|
||||
<div class="flex flex-row gap-2 justify-between sm:contents sm:[&>*]:pr-2">
|
||||
<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 class="log-entry-table-row-cell justify-end">
|
||||
<%= 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">
|
||||
<div class="log-entry-table-row-cell sm:text-right sm:min-w-0">
|
||||
<% 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">
|
||||
<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 -%>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-entry-table-row-cell text-center">
|
||||
<%= 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" } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="log-entry-table-row-cell">
|
||||
<span class="max-w-24 truncate inline-block" title="<%= hle.content_type %>">
|
||||
<%= hle.content_type.split(";")[0] %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="justify-end log-entry-table-row-cell">
|
||||
<%= hle.response_time_ms %>ms
|
||||
<div class="flex flex-row gap-2 justify-between sm:contents sm:[&>*]:pr-2">
|
||||
<div class="log-entry-table-row-cell sm:text-center">
|
||||
<%= 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" } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="log-entry-table-row-cell">
|
||||
<span class="max-w-24 truncate inline-block" title="<%= hle.content_type %>">
|
||||
<%= hle.content_type.split(";")[0] %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="justify-end log-entry-table-row-cell">
|
||||
<%= hle.response_time_ms %>ms
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -3,7 +3,7 @@ if Rails.env.development? || Rails.env.staging?
|
||||
# Rack::MiniProfiler.config.pre_authorize_cb = lambda { |env| true }
|
||||
Rack::MiniProfiler.config.authorization_mode = :allow_all
|
||||
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.skip_paths = [%r{/blobs/.+/contents.jpg$}]
|
||||
end
|
||||
|
||||
@@ -25,7 +25,9 @@ module.exports = {
|
||||
experimental: {
|
||||
classRegex: [
|
||||
/\\bclass:\s*'([^']*)'/,
|
||||
/\\bclass=\s*'([^']*)'/,
|
||||
/\\bclass:\s*"([^"]*)"/,
|
||||
/\\bclass=\s*"([^"]*)"/,
|
||||
/["'`]([^"'`]*).*?,?\s?/,
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user