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>
|
</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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
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 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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ module.exports = {
|
|||||||
experimental: {
|
experimental: {
|
||||||
classRegex: [
|
classRegex: [
|
||||||
/\\bclass:\s*'([^']*)'/,
|
/\\bclass:\s*'([^']*)'/,
|
||||||
|
/\\bclass=\s*'([^']*)'/,
|
||||||
/\\bclass:\s*"([^"]*)"/,
|
/\\bclass:\s*"([^"]*)"/,
|
||||||
|
/\\bclass=\s*"([^"]*)"/,
|
||||||
/["'`]([^"'`]*).*?,?\s?/,
|
/["'`]([^"'`]*).*?,?\s?/,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user