Files
wtrinkl b1363aeab9 Initial commit: Any-LLM Gateway with Laravel Admin Interface
- Any-LLM Gateway setup with Docker Compose
- Laravel 11 admin interface with Livewire
- Dashboard with usage statistics and charts
- Gateway Users management with budget tracking
- API Keys management with revocation
- Budget templates with assignment
- Usage Logs with filtering and CSV export
- Model Pricing management with calculator
- PostgreSQL database integration
- Complete authentication system for admins
2025-11-16 12:38:05 +01:00

315 lines
16 KiB
PHP

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Gateway Users') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<!-- Header -->
<div class="mb-6 flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-900">Gateway Users</h1>
<p class="mt-1 text-sm text-gray-600">Manage API consumers and their access</p>
</div>
<a href="{{ route('gateway-users.create') }}"
class="inline-flex items-center px-4 py-2 bg-indigo-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-indigo-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
Create User
</a>
</div>
<!-- Success Message -->
@if(session('success'))
<div class="mb-6 bg-green-50 border-l-4 border-green-400 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-green-700">{{ session('success') }}</p>
</div>
</div>
</div>
@endif
<!-- Filters -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<form method="GET" action="{{ route('gateway-users.index') }}" class="grid grid-cols-1 md:grid-cols-4 gap-4" id="filterForm">
<div>
<label for="search" class="block text-sm font-medium text-gray-700">Search</label>
<input type="text"
name="search"
id="search"
value="{{ request('search') }}"
placeholder="User ID or Alias..."
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
</div>
<div>
<label for="status" class="block text-sm font-medium text-gray-700">Status</label>
<select name="status"
id="status"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<option value="">All Users</option>
<option value="active" {{ request('status') == 'active' ? 'selected' : '' }}>Active</option>
<option value="blocked" {{ request('status') == 'blocked' ? 'selected' : '' }}>Blocked</option>
</select>
</div>
<div>
<label for="sort" class="block text-sm font-medium text-gray-700">Sort By</label>
<select name="sort"
id="sort"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<option value="created_at" {{ request('sort') == 'created_at' ? 'selected' : '' }}>Created Date</option>
<option value="spend" {{ request('sort') == 'spend' ? 'selected' : '' }}>Spend</option>
<option value="alias" {{ request('sort') == 'alias' ? 'selected' : '' }}>Alias</option>
</select>
</div>
<div class="flex items-end">
<button type="submit"
class="w-full inline-flex justify-center items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700">
Apply Filters
</button>
</div>
</form>
</div>
<!-- Bulk Actions Bar (hidden by default) -->
<div id="bulkActionsBar" class="bg-indigo-50 border border-indigo-200 rounded-lg p-4 mb-6 hidden">
<div class="flex items-center justify-between">
<div class="flex items-center">
<span class="text-sm font-medium text-gray-700 mr-4">
<span id="selectedCount">0</span> user(s) selected
</span>
</div>
<div class="flex items-center space-x-3">
<form id="bulkActionForm" method="POST" action="{{ route('gateway-users.bulk-action') }}" class="flex items-center space-x-3">
@csrf
<input type="hidden" name="action" id="bulkActionType">
<div id="selectedUserIds"></div>
<button type="button"
onclick="executeBulkAction('block')"
class="inline-flex items-center px-3 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-700">
Block Selected
</button>
<button type="button"
onclick="executeBulkAction('unblock')"
class="inline-flex items-center px-3 py-2 bg-green-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-green-700">
Unblock Selected
</button>
<button type="button"
onclick="executeBulkAction('delete')"
class="inline-flex items-center px-3 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700">
Delete Selected
</button>
</form>
</div>
</div>
</div>
<!-- Users Table -->
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left">
<input type="checkbox"
id="selectAll"
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
onchange="toggleAllUsers(this)">
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Budget</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Spend</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">API Keys</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Requests</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@forelse($users as $user)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<input type="checkbox"
class="user-checkbox rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
value="{{ $user->user_id }}"
onchange="updateBulkActions()">
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 flex items-center justify-center bg-indigo-100 rounded-full">
<span class="text-indigo-600 font-semibold text-sm">
{{ strtoupper(substr($user->alias ?? $user->user_id, 0, 2)) }}
</span>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">
{{ $user->alias ?? 'N/A' }}
</div>
<div class="text-sm text-gray-500 font-mono">
{{ substr($user->user_id, 0, 20) }}...
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@if($user->budget)
<div class="text-sm text-gray-900">{{ $user->budget->budget_id }}</div>
<div class="text-sm text-gray-500">${{ number_format($user->budget->max_budget, 2) }}</div>
@else
<span class="text-sm text-gray-400">No budget</span>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-semibold text-gray-900">${{ number_format($user->spend, 2) }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ $user->api_keys_count }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ number_format($user->usage_logs_count) }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@if($user->blocked)
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
Blocked
</span>
@else
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Active
</span>
@endif
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="{{ route('gateway-users.show', $user->user_id) }}"
class="text-indigo-600 hover:text-indigo-900 mr-3">View</a>
<a href="{{ route('gateway-users.edit', $user->user_id) }}"
class="text-yellow-600 hover:text-yellow-900 mr-3">Edit</a>
<form action="{{ route('gateway-users.toggle-block', $user->user_id) }}"
method="POST"
class="inline-block">
@csrf
<button type="submit"
class="text-{{ $user->blocked ? 'green' : 'red' }}-600 hover:text-{{ $user->blocked ? 'green' : 'red' }}-900">
{{ $user->blocked ? 'Unblock' : 'Block' }}
</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="px-6 py-12 text-center">
<div class="flex flex-col items-center">
<svg class="w-16 h-16 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
</svg>
<h3 class="text-lg font-medium text-gray-900 mb-1">No users found</h3>
<p class="text-sm text-gray-500 mb-4">Get started by creating your first gateway user.</p>
<a href="{{ route('gateway-users.create') }}"
class="inline-flex items-center px-4 py-2 bg-indigo-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-indigo-700">
Create User
</a>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Pagination -->
@if($users->hasPages())
<div class="mt-6">
{{ $users->links() }}
</div>
@endif
</div>
</div>
<script>
// Bulk Actions JavaScript
function toggleAllUsers(checkbox) {
const userCheckboxes = document.querySelectorAll('.user-checkbox');
userCheckboxes.forEach(cb => {
cb.checked = checkbox.checked;
});
updateBulkActions();
}
function updateBulkActions() {
const checkedBoxes = document.querySelectorAll('.user-checkbox:checked');
const bulkActionsBar = document.getElementById('bulkActionsBar');
const selectedCount = document.getElementById('selectedCount');
const selectAll = document.getElementById('selectAll');
// Update count
selectedCount.textContent = checkedBoxes.length;
// Show/hide bulk actions bar
if (checkedBoxes.length > 0) {
bulkActionsBar.classList.remove('hidden');
} else {
bulkActionsBar.classList.add('hidden');
}
// Update "Select All" checkbox state
const allCheckboxes = document.querySelectorAll('.user-checkbox');
if (checkedBoxes.length === 0) {
selectAll.checked = false;
selectAll.indeterminate = false;
} else if (checkedBoxes.length === allCheckboxes.length) {
selectAll.checked = true;
selectAll.indeterminate = false;
} else {
selectAll.checked = false;
selectAll.indeterminate = true;
}
}
function executeBulkAction(action) {
const checkedBoxes = document.querySelectorAll('.user-checkbox:checked');
if (checkedBoxes.length === 0) {
alert('Please select at least one user.');
return;
}
// Confirmation for delete action
if (action === 'delete') {
if (!confirm(`Are you sure you want to delete ${checkedBoxes.length} user(s)? This will also delete all associated API keys and usage logs. This action cannot be undone!`)) {
return;
}
}
// Set action type
document.getElementById('bulkActionType').value = action;
// Add selected user IDs as hidden inputs
const selectedUserIds = document.getElementById('selectedUserIds');
selectedUserIds.innerHTML = '';
checkedBoxes.forEach(checkbox => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'user_ids[]';
input.value = checkbox.value;
selectedUserIds.appendChild(input);
});
// Submit form
document.getElementById('bulkActionForm').submit();
}
</script>
</x-app-layout>