fix: lint errors

This commit is contained in:
Feng Yue
2025-08-13 13:10:31 +08:00
parent eb150b4937
commit f193db926d
9 changed files with 1214 additions and 541 deletions

View File

@@ -1,40 +1,59 @@
<template> <template>
<div v-if="show" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"> <div
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white"> v-if="show"
<div class="mt-3"> class="fixed inset-0 z-50 h-full w-full overflow-y-auto bg-gray-600 bg-opacity-50"
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-gray-900">Change User Role</h3>
<button
@click="$emit('close')"
class="text-gray-400 hover:text-gray-600"
> >
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <div class="relative top-20 mx-auto w-96 rounded-md border bg-white p-5 shadow-lg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <div class="mt-3">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900">Change User Role</h3>
<button class="text-gray-400 hover:text-gray-600" @click="$emit('close')">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M6 18L18 6M6 6l12 12"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</button> </button>
</div> </div>
<div v-if="user" class="space-y-4"> <div v-if="user" class="space-y-4">
<!-- User Info --> <!-- User Info -->
<div class="bg-gray-50 p-4 rounded-md"> <div class="rounded-md bg-gray-50 p-4">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<div class="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center"> <div class="flex h-10 w-10 items-center justify-center rounded-full bg-gray-300">
<svg class="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> class="h-6 w-6 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
</div> </div>
<div class="ml-4"> <div class="ml-4">
<p class="text-sm font-medium text-gray-900">{{ user.displayName || user.username }}</p> <p class="text-sm font-medium text-gray-900">
{{ user.displayName || user.username }}
</p>
<p class="text-sm text-gray-500">@{{ user.username }}</p> <p class="text-sm text-gray-500">@{{ user.username }}</p>
<div class="mt-1"> <div class="mt-1">
<span :class="[ <span
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium', :class="[
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
user.role === 'admin' user.role === 'admin'
? 'bg-purple-100 text-purple-800' ? 'bg-purple-100 text-purple-800'
: 'bg-blue-100 text-blue-800' : 'bg-blue-100 text-blue-800'
]"> ]"
>
Current: {{ user.role }} Current: {{ user.role }}
</span> </span>
</div> </div>
@@ -43,19 +62,17 @@
</div> </div>
<!-- Role Selection --> <!-- Role Selection -->
<form @submit.prevent="handleSubmit" class="space-y-4"> <form class="space-y-4" @submit.prevent="handleSubmit">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2"> <label class="mb-2 block text-sm font-medium text-gray-700"> New Role </label>
New Role
</label>
<div class="space-y-2"> <div class="space-y-2">
<label class="flex items-center"> <label class="flex items-center">
<input <input
v-model="selectedRole" v-model="selectedRole"
class="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
:disabled="loading"
type="radio" type="radio"
value="user" value="user"
:disabled="loading"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300"
/> />
<div class="ml-3"> <div class="ml-3">
<div class="text-sm font-medium text-gray-900">User</div> <div class="text-sm font-medium text-gray-900">User</div>
@@ -65,10 +82,10 @@
<label class="flex items-center"> <label class="flex items-center">
<input <input
v-model="selectedRole" v-model="selectedRole"
class="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
:disabled="loading"
type="radio" type="radio"
value="admin" value="admin"
:disabled="loading"
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300"
/> />
<div class="ml-3"> <div class="ml-3">
<div class="text-sm font-medium text-gray-900">Administrator</div> <div class="text-sm font-medium text-gray-900">Administrator</div>
@@ -79,32 +96,45 @@
</div> </div>
<!-- Warning for role changes --> <!-- Warning for role changes -->
<div v-if="selectedRole !== user.role" class="bg-yellow-50 border border-yellow-200 rounded-md p-4"> <div
v-if="selectedRole !== user.role"
class="rounded-md border border-yellow-200 bg-yellow-50 p-4"
>
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /> <path
clip-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
fill-rule="evenodd"
/>
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">Role Change Warning</h3> <h3 class="text-sm font-medium text-yellow-800">Role Change Warning</h3>
<div class="mt-2 text-sm text-yellow-700"> <div class="mt-2 text-sm text-yellow-700">
<p v-if="selectedRole === 'admin'"> <p v-if="selectedRole === 'admin'">
Granting admin privileges will give this user full access to the system, including the ability to manage other users and their API keys. Granting admin privileges will give this user full access to the system,
including the ability to manage other users and their API keys.
</p> </p>
<p v-else> <p v-else>
Removing admin privileges will restrict this user to only managing their own API keys and viewing their own usage statistics. Removing admin privileges will restrict this user to only managing their own
API keys and viewing their own usage statistics.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div v-if="error" class="bg-red-50 border border-red-200 rounded-md p-4"> <div v-if="error" class="rounded-md border border-red-200 bg-red-50 p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> <path
clip-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
fill-rule="evenodd"
/>
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
@@ -115,22 +145,38 @@
<div class="flex justify-end space-x-3 pt-4"> <div class="flex justify-end space-x-3 pt-4">
<button <button
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50"
:disabled="loading"
type="button" type="button"
@click="$emit('close')" @click="$emit('close')"
:disabled="loading"
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" class="rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="loading || selectedRole === user.role" :disabled="loading || selectedRole === user.role"
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" type="submit"
> >
<span v-if="loading" class="flex items-center"> <span v-if="loading" class="flex items-center">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> class="-ml-1 mr-2 h-4 w-4 animate-spin text-white"
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
></path>
</svg> </svg>
Updating... Updating...
</span> </span>

View File

@@ -1,20 +1,25 @@
<template> <template>
<div v-if="show" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"> <div
<div class="relative top-10 mx-auto p-5 border w-4/5 max-w-4xl shadow-lg rounded-md bg-white"> v-if="show"
class="fixed inset-0 z-50 h-full w-full overflow-y-auto bg-gray-600 bg-opacity-50"
>
<div class="relative top-10 mx-auto w-4/5 max-w-4xl rounded-md border bg-white p-5 shadow-lg">
<div class="mt-3"> <div class="mt-3">
<div class="flex items-center justify-between mb-6"> <div class="mb-6 flex items-center justify-between">
<div> <div>
<h3 class="text-lg font-medium text-gray-900"> <h3 class="text-lg font-medium text-gray-900">
Usage Statistics - {{ user?.displayName || user?.username }} Usage Statistics - {{ user?.displayName || user?.username }}
</h3> </h3>
<p class="text-sm text-gray-500">@{{ user?.username }} {{ user?.role }}</p> <p class="text-sm text-gray-500">@{{ user?.username }} {{ user?.role }}</p>
</div> </div>
<button <button class="text-gray-400 hover:text-gray-600" @click="emit('close')">
@click="$emit('close')" <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
class="text-gray-400 hover:text-gray-600" <path
> d="M6 18L18 6M6 6l12 12"
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> stroke-linecap="round"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -23,8 +28,8 @@
<div class="mb-6"> <div class="mb-6">
<select <select
v-model="selectedPeriod" v-model="selectedPeriod"
@change="loadUsageStats"
class="block w-32 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" class="block w-32 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
@change="loadUsageStats"
> >
<option value="day">Last 24 Hours</option> <option value="day">Last 24 Hours</option>
<option value="week">Last 7 Days</option> <option value="week">Last 7 Days</option>
@@ -34,10 +39,26 @@
</div> </div>
<!-- Loading State --> <!-- Loading State -->
<div v-if="loading" class="text-center py-12"> <div v-if="loading" class="py-12 text-center">
<svg class="animate-spin h-8 w-8 text-blue-600 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> class="mx-auto h-8 w-8 animate-spin text-blue-600"
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
></path>
</svg> </svg>
<p class="mt-2 text-sm text-gray-500">Loading usage statistics...</p> <p class="mt-2 text-sm text-gray-500">Loading usage statistics...</p>
</div> </div>
@@ -45,73 +66,121 @@
<!-- Stats Content --> <!-- Stats Content -->
<div v-else class="space-y-6"> <div v-else class="space-y-6">
<!-- Summary Cards --> <!-- Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<div class="bg-blue-50 overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-blue-50 shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> class="h-6 w-6 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M13 10V3L4 14h7v7l9-11h-7z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-blue-600 truncate">Requests</dt> <dt class="truncate text-sm font-medium text-blue-600">Requests</dt>
<dd class="text-lg font-medium text-blue-900">{{ formatNumber(usageStats?.totalRequests || 0) }}</dd> <dd class="text-lg font-medium text-blue-900">
{{ formatNumber(usageStats?.totalRequests || 0) }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-green-50 overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-green-50 shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> class="h-6 w-6 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-green-600 truncate">Input Tokens</dt> <dt class="truncate text-sm font-medium text-green-600">Input Tokens</dt>
<dd class="text-lg font-medium text-green-900">{{ formatNumber(usageStats?.totalInputTokens || 0) }}</dd> <dd class="text-lg font-medium text-green-900">
{{ formatNumber(usageStats?.totalInputTokens || 0) }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-purple-50 overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-purple-50 shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> class="h-6 w-6 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-purple-600 truncate">Output Tokens</dt> <dt class="truncate text-sm font-medium text-purple-600">Output Tokens</dt>
<dd class="text-lg font-medium text-purple-900">{{ formatNumber(usageStats?.totalOutputTokens || 0) }}</dd> <dd class="text-lg font-medium text-purple-900">
{{ formatNumber(usageStats?.totalOutputTokens || 0) }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-yellow-50 overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-yellow-50 shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-yellow-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" /> class="h-6 w-6 text-yellow-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-yellow-600 truncate">Total Cost</dt> <dt class="truncate text-sm font-medium text-yellow-600">Total Cost</dt>
<dd class="text-lg font-medium text-yellow-900">${{ (usageStats?.totalCost || 0).toFixed(4) }}</dd> <dd class="text-lg font-medium text-yellow-900">
${{ (usageStats?.totalCost || 0).toFixed(4) }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
@@ -120,61 +189,84 @@
</div> </div>
<!-- User API Keys Table --> <!-- User API Keys Table -->
<div v-if="userDetails?.apiKeys?.length > 0" class="bg-white border border-gray-200 rounded-lg"> <div
<div class="px-4 py-5 sm:px-6 border-b border-gray-200"> v-if="userDetails?.apiKeys?.length > 0"
<h4 class="text-lg leading-6 font-medium text-gray-900">API Keys Usage</h4> class="rounded-lg border border-gray-200 bg-white"
>
<div class="border-b border-gray-200 px-4 py-5 sm:px-6">
<h4 class="text-lg font-medium leading-6 text-gray-900">API Keys Usage</h4>
</div> </div>
<div class="overflow-hidden"> <div class="overflow-hidden">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
scope="col"
>
API Key API Key
</th> </th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
scope="col"
>
Status Status
</th> </th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
scope="col"
>
Requests Requests
</th> </th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
scope="col"
>
Tokens Tokens
</th> </th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
scope="col"
>
Cost Cost
</th> </th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
scope="col"
>
Last Used Last Used
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="bg-white divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 bg-white">
<tr v-for="apiKey in userDetails.apiKeys" :key="apiKey.id"> <tr v-for="apiKey in userDetails.apiKeys" :key="apiKey.id">
<td class="px-6 py-4 whitespace-nowrap"> <td class="whitespace-nowrap px-6 py-4">
<div class="text-sm font-medium text-gray-900">{{ apiKey.name }}</div> <div class="text-sm font-medium text-gray-900">{{ apiKey.name }}</div>
<div class="text-sm text-gray-500">{{ apiKey.keyPreview }}</div> <div class="text-sm text-gray-500">{{ apiKey.keyPreview }}</div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="whitespace-nowrap px-6 py-4">
<span :class="[ <span
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium', :class="[
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
apiKey.isActive apiKey.isActive
? 'bg-green-100 text-green-800' ? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800' : 'bg-red-100 text-red-800'
]"> ]"
>
{{ apiKey.isActive ? 'Active' : 'Disabled' }} {{ apiKey.isActive ? 'Active' : 'Disabled' }}
</span> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
{{ formatNumber(apiKey.usage?.requests || 0) }} {{ formatNumber(apiKey.usage?.requests || 0) }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
<div>In: {{ formatNumber(apiKey.usage?.inputTokens || 0) }}</div> <div>In: {{ formatNumber(apiKey.usage?.inputTokens || 0) }}</div>
<div>Out: {{ formatNumber(apiKey.usage?.outputTokens || 0) }}</div> <div>Out: {{ formatNumber(apiKey.usage?.outputTokens || 0) }}</div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
${{ (apiKey.usage?.totalCost || 0).toFixed(4) }} ${{ (apiKey.usage?.totalCost || 0).toFixed(4) }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{{ apiKey.lastUsedAt ? formatDate(apiKey.lastUsedAt) : 'Never' }} {{ apiKey.lastUsedAt ? formatDate(apiKey.lastUsedAt) : 'Never' }}
</td> </td>
</tr> </tr>
@@ -184,15 +276,27 @@
</div> </div>
<!-- Chart Placeholder --> <!-- Chart Placeholder -->
<div class="bg-white border border-gray-200 rounded-lg"> <div class="rounded-lg border border-gray-200 bg-white">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200"> <div class="border-b border-gray-200 px-4 py-5 sm:px-6">
<h4 class="text-lg leading-6 font-medium text-gray-900">Usage Trend</h4> <h4 class="text-lg font-medium leading-6 text-gray-900">Usage Trend</h4>
</div> </div>
<div class="p-6"> <div class="p-6">
<div class="border-2 border-dashed border-gray-300 rounded-lg h-64 flex items-center justify-center"> <div
class="flex h-64 items-center justify-center rounded-lg border-2 border-dashed border-gray-300"
>
<div class="text-center"> <div class="text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> class="mx-auto h-12 w-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">Usage Chart</h3> <h3 class="mt-2 text-sm font-medium text-gray-900">Usage Chart</h3>
<p class="mt-1 text-sm text-gray-500"> <p class="mt-1 text-sm text-gray-500">
@@ -207,9 +311,19 @@
</div> </div>
<!-- No Data State --> <!-- No Data State -->
<div v-if="usageStats && usageStats.totalRequests === 0" class="text-center py-12"> <div v-if="usageStats && usageStats.totalRequests === 0" class="py-12 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> class="mx-auto h-12 w-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No usage data</h3> <h3 class="mt-2 text-sm font-medium text-gray-900">No usage data</h3>
<p class="mt-1 text-sm text-gray-500"> <p class="mt-1 text-sm text-gray-500">
@@ -218,10 +332,10 @@
</div> </div>
</div> </div>
<div class="flex justify-end mt-6"> <div class="mt-6 flex justify-end">
<button <button
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
@click="$emit('close')" @click="$emit('close')"
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
> >
Close Close
</button> </button>

View File

@@ -1,106 +1,113 @@
<template> <template>
<div v-if="show" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"> <div
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white"> v-if="show"
<div class="mt-3"> class="fixed inset-0 z-50 h-full w-full overflow-y-auto bg-gray-600 bg-opacity-50"
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-gray-900">Create New API Key</h3>
<button
@click="$emit('close')"
class="text-gray-400 hover:text-gray-600"
> >
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <div class="relative top-20 mx-auto w-96 rounded-md border bg-white p-5 shadow-lg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <div class="mt-3">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900">Create New API Key</h3>
<button class="text-gray-400 hover:text-gray-600" @click="$emit('close')">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M6 18L18 6M6 6l12 12"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</button> </button>
</div> </div>
<form @submit.prevent="handleSubmit" class="space-y-4"> <form class="space-y-4" @submit.prevent="handleSubmit">
<div> <div>
<label for="name" class="block text-sm font-medium text-gray-700"> <label class="block text-sm font-medium text-gray-700" for="name"> Name * </label>
Name *
</label>
<input <input
id="name" id="name"
v-model="form.name" v-model="form.name"
type="text" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
required
:disabled="loading" :disabled="loading"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
placeholder="Enter API key name" placeholder="Enter API key name"
required
type="text"
/> />
</div> </div>
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700"> <label class="block text-sm font-medium text-gray-700" for="description">
Description Description
</label> </label>
<textarea <textarea
id="description" id="description"
v-model="form.description" v-model="form.description"
rows="3" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
:disabled="loading" :disabled="loading"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
placeholder="Optional description" placeholder="Optional description"
rows="3"
></textarea> ></textarea>
</div> </div>
<div> <div>
<label for="tokenLimit" class="block text-sm font-medium text-gray-700"> <label class="block text-sm font-medium text-gray-700" for="tokenLimit">
Token Limit (optional) Token Limit (optional)
</label> </label>
<input <input
id="tokenLimit" id="tokenLimit"
v-model.number="form.tokenLimit" v-model.number="form.tokenLimit"
type="number" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
min="0"
:disabled="loading" :disabled="loading"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" min="0"
placeholder="0 = unlimited" placeholder="0 = unlimited"
type="number"
/> />
<p class="mt-1 text-xs text-gray-500">Set to 0 for unlimited tokens</p> <p class="mt-1 text-xs text-gray-500">Set to 0 for unlimited tokens</p>
</div> </div>
<div> <div>
<label for="dailyCostLimit" class="block text-sm font-medium text-gray-700"> <label class="block text-sm font-medium text-gray-700" for="dailyCostLimit">
Daily Cost Limit (optional) Daily Cost Limit (optional)
</label> </label>
<div class="mt-1 relative rounded-md shadow-sm"> <div class="relative mt-1 rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<span class="text-gray-500 sm:text-sm">$</span> <span class="text-gray-500 sm:text-sm">$</span>
</div> </div>
<input <input
id="dailyCostLimit" id="dailyCostLimit"
v-model.number="form.dailyCostLimit" v-model.number="form.dailyCostLimit"
type="number" class="block w-full rounded-md border-gray-300 pl-7 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
min="0"
step="0.01"
:disabled="loading" :disabled="loading"
class="pl-7 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" min="0"
placeholder="0.00" placeholder="0.00"
step="0.01"
type="number"
/> />
</div> </div>
<p class="mt-1 text-xs text-gray-500">Set to 0 for unlimited daily cost</p> <p class="mt-1 text-xs text-gray-500">Set to 0 for unlimited daily cost</p>
</div> </div>
<div> <div>
<label for="expiresAt" class="block text-sm font-medium text-gray-700"> <label class="block text-sm font-medium text-gray-700" for="expiresAt">
Expiration Date (optional) Expiration Date (optional)
</label> </label>
<input <input
id="expiresAt" id="expiresAt"
v-model="form.expiresAt" v-model="form.expiresAt"
type="datetime-local" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
:disabled="loading" :disabled="loading"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" type="datetime-local"
/> />
<p class="mt-1 text-xs text-gray-500">Leave empty for no expiration</p> <p class="mt-1 text-xs text-gray-500">Leave empty for no expiration</p>
</div> </div>
<div v-if="error" class="bg-red-50 border border-red-200 rounded-md p-3"> <div v-if="error" class="rounded-md border border-red-200 bg-red-50 p-3">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> <path
clip-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
fill-rule="evenodd"
/>
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
@@ -111,22 +118,38 @@
<div class="flex justify-end space-x-3 pt-4"> <div class="flex justify-end space-x-3 pt-4">
<button <button
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50"
:disabled="loading"
type="button" type="button"
@click="$emit('close')" @click="$emit('close')"
:disabled="loading"
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" class="rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="loading || !form.name.trim()" :disabled="loading || !form.name.trim()"
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" type="submit"
> >
<span v-if="loading" class="flex items-center"> <span v-if="loading" class="flex items-center">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> class="-ml-1 mr-2 h-4 w-4 animate-spin text-white"
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
></path>
</svg> </svg>
Creating... Creating...
</span> </span>
@@ -136,28 +159,45 @@
</form> </form>
<!-- Success Modal for showing the new API key --> <!-- Success Modal for showing the new API key -->
<div v-if="newApiKey" class="mt-6 p-4 bg-green-50 border border-green-200 rounded-md"> <div v-if="newApiKey" class="mt-6 rounded-md border border-green-200 bg-green-50 p-4">
<div class="flex items-start"> <div class="flex items-start">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor"> <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" /> <path
clip-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"
fill-rule="evenodd"
/>
</svg> </svg>
</div> </div>
<div class="ml-3 flex-1"> <div class="ml-3 flex-1">
<h4 class="text-sm font-medium text-green-800">API Key Created Successfully!</h4> <h4 class="text-sm font-medium text-green-800">API Key Created Successfully!</h4>
<div class="mt-3"> <div class="mt-3">
<p class="text-sm text-green-700 mb-2"> <p class="mb-2 text-sm text-green-700">
<strong>Important:</strong> Copy your API key now. You won't be able to see it again! <strong>Important:</strong> Copy your API key now. You won't be able to see it
again!
</p> </p>
<div class="bg-white p-3 border border-green-300 rounded-md"> <div class="rounded-md border border-green-300 bg-white p-3">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<code class="text-sm font-mono text-gray-900 break-all">{{ newApiKey.key }}</code> <code class="break-all font-mono text-sm text-gray-900">{{
newApiKey.key
}}</code>
<button <button
class="ml-3 inline-flex flex-shrink-0 items-center rounded border border-transparent bg-green-100 px-2 py-1 text-xs font-medium text-green-700 hover:bg-green-200 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
@click="copyToClipboard(newApiKey.key)" @click="copyToClipboard(newApiKey.key)"
class="ml-3 flex-shrink-0 inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-green-700 bg-green-100 hover:bg-green-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
> >
<svg class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> class="mr-1 h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
Copy Copy
</button> </button>
@@ -166,8 +206,8 @@
</div> </div>
<div class="mt-4 flex justify-end"> <div class="mt-4 flex justify-end">
<button <button
class="rounded-md border border-transparent bg-green-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
@click="handleClose" @click="handleClose"
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
> >
Done Done
</button> </button>
@@ -269,11 +309,14 @@ const handleClose = () => {
} }
// Reset form when modal is shown // Reset form when modal is shown
watch(() => props.show, (newValue) => { watch(
() => props.show,
(newValue) => {
if (newValue) { if (newValue) {
resetForm() resetForm()
} }
}) }
)
</script> </script>
<style scoped> <style scoped>

View File

@@ -7,14 +7,19 @@
Manage your API keys to access Claude Relay services Manage your API keys to access Claude Relay services
</p> </p>
</div> </div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"> <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<button <button
@click="showCreateModal = true" class="inline-flex items-center justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
:disabled="apiKeys.length >= maxApiKeys" :disabled="apiKeys.length >= maxApiKeys"
class="inline-flex items-center justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed sm:w-auto" @click="showCreateModal = true"
> >
<svg class="-ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
Create API Key Create API Key
</button> </button>
@@ -22,48 +27,72 @@
</div> </div>
<!-- API Keys 数量限制提示 --> <!-- API Keys 数量限制提示 -->
<div v-if="apiKeys.length >= maxApiKeys" class="bg-yellow-50 border border-yellow-200 rounded-md p-4"> <div
v-if="apiKeys.length >= maxApiKeys"
class="rounded-md border border-yellow-200 bg-yellow-50 p-4"
>
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /> <path
clip-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
fill-rule="evenodd"
/>
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<p class="text-sm text-yellow-700"> <p class="text-sm text-yellow-700">
You have reached the maximum number of API keys ({{ maxApiKeys }}). You have reached the maximum number of API keys ({{ maxApiKeys }}). Please delete an
Please delete an existing key to create a new one. existing key to create a new one.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<!-- Loading State --> <!-- Loading State -->
<div v-if="loading" class="text-center py-12"> <div v-if="loading" class="py-12 text-center">
<svg class="animate-spin h-8 w-8 text-blue-600 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> class="mx-auto h-8 w-8 animate-spin text-blue-600"
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
></path>
</svg> </svg>
<p class="mt-2 text-sm text-gray-500">Loading API keys...</p> <p class="mt-2 text-sm text-gray-500">Loading API keys...</p>
</div> </div>
<!-- API Keys List --> <!-- API Keys List -->
<div v-else-if="apiKeys.length > 0" class="bg-white shadow overflow-hidden sm:rounded-md"> <div v-else-if="apiKeys.length > 0" class="overflow-hidden bg-white shadow sm:rounded-md">
<ul role="list" class="divide-y divide-gray-200"> <ul class="divide-y divide-gray-200" role="list">
<li v-for="apiKey in apiKeys" :key="apiKey.id" class="px-6 py-4"> <li v-for="apiKey in apiKeys" :key="apiKey.id" class="px-6 py-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<div :class="[ <div
'h-2 w-2 rounded-full', :class="['h-2 w-2 rounded-full', apiKey.isActive ? 'bg-green-400' : 'bg-red-400']"
apiKey.isActive ? 'bg-green-400' : 'bg-red-400' ></div>
]"></div>
</div> </div>
<div class="ml-4"> <div class="ml-4">
<div class="flex items-center"> <div class="flex items-center">
<p class="text-sm font-medium text-gray-900">{{ apiKey.name }}</p> <p class="text-sm font-medium text-gray-900">{{ apiKey.name }}</p>
<span v-if="!apiKey.isActive" <span
class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800"> v-if="!apiKey.isActive"
class="ml-2 inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800"
>
Disabled Disabled
</span> </span>
</div> </div>
@@ -71,7 +100,9 @@
<p class="text-sm text-gray-500">{{ apiKey.description || 'No description' }}</p> <p class="text-sm text-gray-500">{{ apiKey.description || 'No description' }}</p>
<div class="mt-1 flex items-center space-x-4 text-xs text-gray-400"> <div class="mt-1 flex items-center space-x-4 text-xs text-gray-400">
<span>Created: {{ formatDate(apiKey.createdAt) }}</span> <span>Created: {{ formatDate(apiKey.createdAt) }}</span>
<span v-if="apiKey.lastUsedAt">Last used: {{ formatDate(apiKey.lastUsedAt) }}</span> <span v-if="apiKey.lastUsedAt"
>Last used: {{ formatDate(apiKey.lastUsedAt) }}</span
>
<span v-else>Never used</span> <span v-else>Never used</span>
<span v-if="apiKey.expiresAt">Expires: {{ formatDate(apiKey.expiresAt) }}</span> <span v-if="apiKey.expiresAt">Expires: {{ formatDate(apiKey.expiresAt) }}</span>
</div> </div>
@@ -88,33 +119,53 @@
<!-- Actions --> <!-- Actions -->
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<button <button
@click="showApiKey(apiKey)" class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-gray-600"
class="inline-flex items-center p-1 border border-transparent rounded text-gray-400 hover:text-gray-600"
title="View API Key" title="View API Key"
@click="showApiKey(apiKey)"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <path
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
<path
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</button> </button>
<button <button
@click="regenerateApiKey(apiKey)" class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-gray-600"
class="inline-flex items-center p-1 border border-transparent rounded text-gray-400 hover:text-gray-600"
title="Regenerate API Key" title="Regenerate API Key"
@click="regenerateApiKey(apiKey)"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</button> </button>
<button <button
@click="deleteApiKey(apiKey)" class="inline-flex items-center rounded border border-transparent p-1 text-red-400 hover:text-red-600"
class="inline-flex items-center p-1 border border-transparent rounded text-red-400 hover:text-red-600"
title="Delete API Key" title="Delete API Key"
@click="deleteApiKey(apiKey)"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> <path
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -125,19 +176,34 @@
</div> </div>
<!-- Empty State --> <!-- Empty State -->
<div v-else class="text-center py-12"> <div v-else class="py-12 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m0 0a2 2 0 012 2m-2-2h-6m6 0v6a2 2 0 01-2 2H9a2 2 0 01-2-2V9a2 2 0 012-2h6z" /> class="mx-auto h-12 w-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M15 7a2 2 0 012 2m0 0a2 2 0 012 2m-2-2h-6m6 0v6a2 2 0 01-2 2H9a2 2 0 01-2-2V9a2 2 0 012-2h6z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No API keys</h3> <h3 class="mt-2 text-sm font-medium text-gray-900">No API keys</h3>
<p class="mt-1 text-sm text-gray-500">Get started by creating your first API key.</p> <p class="mt-1 text-sm text-gray-500">Get started by creating your first API key.</p>
<div class="mt-6"> <div class="mt-6">
<button <button
class="inline-flex items-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
@click="showCreateModal = true" @click="showCreateModal = true"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
> >
<svg class="-ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
Create API Key Create API Key
</button> </button>
@@ -153,20 +219,20 @@
<!-- View API Key Modal --> <!-- View API Key Modal -->
<ViewApiKeyModal <ViewApiKeyModal
:api-key="selectedApiKey"
:show="showViewModal" :show="showViewModal"
:apiKey="selectedApiKey"
@close="showViewModal = false" @close="showViewModal = false"
/> />
<!-- Confirm Delete Modal --> <!-- Confirm Delete Modal -->
<ConfirmModal <ConfirmModal
confirm-class="bg-red-600 hover:bg-red-700"
confirm-text="Delete"
:message="`Are you sure you want to delete '${selectedApiKey?.name}'? This action cannot be undone.`"
:show="showDeleteModal" :show="showDeleteModal"
title="Delete API Key" title="Delete API Key"
:message="`Are you sure you want to delete '${selectedApiKey?.name}'? This action cannot be undone.`"
confirmText="Delete"
confirmClass="bg-red-600 hover:bg-red-700"
@confirm="handleDeleteConfirm"
@cancel="showDeleteModal = false" @cancel="showDeleteModal = false"
@confirm="handleDeleteConfirm"
/> />
</div> </div>
</template> </template>

View File

@@ -3,15 +3,13 @@
<div class="sm:flex sm:items-center"> <div class="sm:flex sm:items-center">
<div class="sm:flex-auto"> <div class="sm:flex-auto">
<h1 class="text-2xl font-semibold text-gray-900">Usage Statistics</h1> <h1 class="text-2xl font-semibold text-gray-900">Usage Statistics</h1>
<p class="mt-2 text-sm text-gray-700"> <p class="mt-2 text-sm text-gray-700">View your API usage statistics and costs</p>
View your API usage statistics and costs
</p>
</div> </div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"> <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<select <select
v-model="selectedPeriod" v-model="selectedPeriod"
@change="loadUsageStats"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
@change="loadUsageStats"
> >
<option value="day">Last 24 Hours</option> <option value="day">Last 24 Hours</option>
<option value="week">Last 7 Days</option> <option value="week">Last 7 Days</option>
@@ -22,82 +20,146 @@
</div> </div>
<!-- Loading State --> <!-- Loading State -->
<div v-if="loading" class="text-center py-12"> <div v-if="loading" class="py-12 text-center">
<svg class="animate-spin h-8 w-8 text-blue-600 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> class="mx-auto h-8 w-8 animate-spin text-blue-600"
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
></path>
</svg> </svg>
<p class="mt-2 text-sm text-gray-500">Loading usage statistics...</p> <p class="mt-2 text-sm text-gray-500">Loading usage statistics...</p>
</div> </div>
<!-- Stats Cards --> <!-- Stats Cards -->
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div v-else class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> class="h-6 w-6 text-blue-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M13 10V3L4 14h7v7l9-11h-7z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Requests</dt> <dt class="truncate text-sm font-medium text-gray-500">Total Requests</dt>
<dd class="text-lg font-medium text-gray-900">{{ formatNumber(usageStats?.totalRequests || 0) }}</dd> <dd class="text-lg font-medium text-gray-900">
{{ formatNumber(usageStats?.totalRequests || 0) }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> class="h-6 w-6 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate">Input Tokens</dt> <dt class="truncate text-sm font-medium text-gray-500">Input Tokens</dt>
<dd class="text-lg font-medium text-gray-900">{{ formatNumber(usageStats?.totalInputTokens || 0) }}</dd> <dd class="text-lg font-medium text-gray-900">
{{ formatNumber(usageStats?.totalInputTokens || 0) }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-purple-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> class="h-6 w-6 text-purple-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate">Output Tokens</dt> <dt class="truncate text-sm font-medium text-gray-500">Output Tokens</dt>
<dd class="text-lg font-medium text-gray-900">{{ formatNumber(usageStats?.totalOutputTokens || 0) }}</dd> <dd class="text-lg font-medium text-gray-900">
{{ formatNumber(usageStats?.totalOutputTokens || 0) }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" /> class="h-6 w-6 text-yellow-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Cost</dt> <dt class="truncate text-sm font-medium text-gray-500">Total Cost</dt>
<dd class="text-lg font-medium text-gray-900">${{ (usageStats?.totalCost || 0).toFixed(4) }}</dd> <dd class="text-lg font-medium text-gray-900">
${{ (usageStats?.totalCost || 0).toFixed(4) }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
@@ -106,20 +168,30 @@
</div> </div>
<!-- Daily Usage Chart --> <!-- Daily Usage Chart -->
<div v-if="!loading && usageStats" class="bg-white shadow rounded-lg"> <div v-if="!loading && usageStats" class="rounded-lg bg-white shadow">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Daily Usage Trend</h3> <h3 class="mb-4 text-lg font-medium leading-6 text-gray-900">Daily Usage Trend</h3>
<!-- Placeholder for chart - you can integrate Chart.js or similar --> <!-- Placeholder for chart - you can integrate Chart.js or similar -->
<div class="border-2 border-dashed border-gray-300 rounded-lg h-64 flex items-center justify-center"> <div
class="flex h-64 items-center justify-center rounded-lg border-2 border-dashed border-gray-300"
>
<div class="text-center"> <div class="text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> class="mx-auto h-12 w-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">Usage Chart</h3> <h3 class="mt-2 text-sm font-medium text-gray-900">Usage Chart</h3>
<p class="mt-1 text-sm text-gray-500"> <p class="mt-1 text-sm text-gray-500">Daily usage trends would be displayed here</p>
Daily usage trends would be displayed here
</p>
<p class="mt-2 text-xs text-gray-400"> <p class="mt-2 text-xs text-gray-400">
(Chart integration can be added with Chart.js, D3.js, or similar library) (Chart integration can be added with Chart.js, D3.js, or similar library)
</p> </p>
@@ -129,14 +201,21 @@
</div> </div>
<!-- Model Usage Breakdown --> <!-- Model Usage Breakdown -->
<div v-if="!loading && usageStats && usageStats.modelStats?.length > 0" class="bg-white shadow rounded-lg"> <div
v-if="!loading && usageStats && usageStats.modelStats?.length > 0"
class="rounded-lg bg-white shadow"
>
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Usage by Model</h3> <h3 class="mb-4 text-lg font-medium leading-6 text-gray-900">Usage by Model</h3>
<div class="space-y-3"> <div class="space-y-3">
<div v-for="model in usageStats.modelStats" :key="model.name" class="flex items-center justify-between"> <div
v-for="model in usageStats.modelStats"
:key="model.name"
class="flex items-center justify-between"
>
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<div class="h-2 w-2 bg-blue-500 rounded-full"></div> <div class="h-2 w-2 rounded-full bg-blue-500"></div>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<p class="text-sm font-medium text-gray-900">{{ model.name }}</p> <p class="text-sm font-medium text-gray-900">{{ model.name }}</p>
@@ -152,58 +231,76 @@
</div> </div>
<!-- Detailed Usage Table --> <!-- Detailed Usage Table -->
<div v-if="!loading && userApiKeys.length > 0" class="bg-white shadow rounded-lg"> <div v-if="!loading && userApiKeys.length > 0" class="rounded-lg bg-white shadow">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Usage by API Key</h3> <h3 class="mb-4 text-lg font-medium leading-6 text-gray-900">Usage by API Key</h3>
<div class="overflow-hidden"> <div class="overflow-hidden">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
scope="col"
>
API Key API Key
</th> </th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
scope="col"
>
Requests Requests
</th> </th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
scope="col"
>
Input Tokens Input Tokens
</th> </th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
scope="col"
>
Output Tokens Output Tokens
</th> </th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
scope="col"
>
Cost Cost
</th> </th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
scope="col"
>
Status Status
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="bg-white divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 bg-white">
<tr v-for="apiKey in userApiKeys" :key="apiKey.id"> <tr v-for="apiKey in userApiKeys" :key="apiKey.id">
<td class="px-6 py-4 whitespace-nowrap"> <td class="whitespace-nowrap px-6 py-4">
<div class="text-sm font-medium text-gray-900">{{ apiKey.name }}</div> <div class="text-sm font-medium text-gray-900">{{ apiKey.name }}</div>
<div class="text-sm text-gray-500">{{ apiKey.keyPreview }}</div> <div class="text-sm text-gray-500">{{ apiKey.keyPreview }}</div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
{{ formatNumber(apiKey.usage?.requests || 0) }} {{ formatNumber(apiKey.usage?.requests || 0) }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
{{ formatNumber(apiKey.usage?.inputTokens || 0) }} {{ formatNumber(apiKey.usage?.inputTokens || 0) }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
{{ formatNumber(apiKey.usage?.outputTokens || 0) }} {{ formatNumber(apiKey.usage?.outputTokens || 0) }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
${{ (apiKey.usage?.totalCost || 0).toFixed(4) }} ${{ (apiKey.usage?.totalCost || 0).toFixed(4) }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="whitespace-nowrap px-6 py-4">
<span :class="[ <span
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium', :class="[
apiKey.isActive 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
? 'bg-green-100 text-green-800' apiKey.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
: 'bg-red-100 text-red-800' ]"
]"> >
{{ apiKey.isActive ? 'Active' : 'Disabled' }} {{ apiKey.isActive ? 'Active' : 'Disabled' }}
</span> </span>
</td> </td>
@@ -215,13 +312,27 @@
</div> </div>
<!-- No Data State --> <!-- No Data State -->
<div v-if="!loading && (!usageStats || usageStats.totalRequests === 0)" class="text-center py-12"> <div
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> v-if="!loading && (!usageStats || usageStats.totalRequests === 0)"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> class="py-12 text-center"
>
<svg
class="mx-auto h-12 w-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No usage data</h3> <h3 class="mt-2 text-sm font-medium text-gray-900">No usage data</h3>
<p class="mt-1 text-sm text-gray-500"> <p class="mt-1 text-sm text-gray-500">
You haven't made any API requests yet. Create an API key and start using the service to see usage statistics. You haven't made any API requests yet. Create an API key and start using the service to see
usage statistics.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,15 +1,20 @@
<template> <template>
<div v-if="show" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"> <div
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white"> v-if="show"
<div class="mt-3"> class="fixed inset-0 z-50 h-full w-full overflow-y-auto bg-gray-600 bg-opacity-50"
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-gray-900">API Key Details</h3>
<button
@click="$emit('close')"
class="text-gray-400 hover:text-gray-600"
> >
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <div class="relative top-20 mx-auto w-96 rounded-md border bg-white p-5 shadow-lg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <div class="mt-3">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900">API Key Details</h3>
<button class="text-gray-400 hover:text-gray-600" @click="emit('close')">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M6 18L18 6M6 6l12 12"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -32,35 +37,71 @@
<label class="block text-sm font-medium text-gray-700">API Key</label> <label class="block text-sm font-medium text-gray-700">API Key</label>
<div class="mt-1 flex items-center space-x-2"> <div class="mt-1 flex items-center space-x-2">
<div class="flex-1"> <div class="flex-1">
<div v-if="showFullKey" class="bg-gray-50 p-3 border border-gray-300 rounded-md"> <div v-if="showFullKey" class="rounded-md border border-gray-300 bg-gray-50 p-3">
<code class="text-sm font-mono text-gray-900 break-all">{{ apiKey.key || 'Not available' }}</code> <code class="break-all font-mono text-sm text-gray-900">{{
apiKey.key || 'Not available'
}}</code>
</div> </div>
<div v-else class="bg-gray-50 p-3 border border-gray-300 rounded-md"> <div v-else class="rounded-md border border-gray-300 bg-gray-50 p-3">
<code class="text-sm font-mono text-gray-900">{{ apiKey.keyPreview || 'cr_****' }}</code> <code class="font-mono text-sm text-gray-900">{{
apiKey.keyPreview || 'cr_****'
}}</code>
</div> </div>
</div> </div>
<div class="flex flex-col space-y-1"> <div class="flex flex-col space-y-1">
<button <button
v-if="apiKey.key" v-if="apiKey.key"
class="inline-flex items-center rounded border border-gray-300 bg-white px-2 py-1 text-xs font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
@click="showFullKey = !showFullKey" @click="showFullKey = !showFullKey"
class="inline-flex items-center px-2 py-1 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
> >
<svg v-if="showFullKey" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L12 12m-1.122-2.122L12 12m-1.122-2.122l-4.243-4.242m6.879 6.878L15 15" /> v-if="showFullKey"
class="mr-1 h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L12 12m-1.122-2.122L12 12m-1.122-2.122l-4.243-4.242m6.879 6.878L15 15"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
<svg v-else class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> v-else
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /> class="mr-1 h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
<path
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
{{ showFullKey ? 'Hide' : 'Show' }} {{ showFullKey ? 'Hide' : 'Show' }}
</button> </button>
<button <button
v-if="showFullKey && apiKey.key" v-if="showFullKey && apiKey.key"
class="inline-flex items-center rounded border border-gray-300 bg-white px-2 py-1 text-xs font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
@click="copyToClipboard(apiKey.key)" @click="copyToClipboard(apiKey.key)"
class="inline-flex items-center px-2 py-1 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
> >
<svg class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="mr-1 h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> <path
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
Copy Copy
</button> </button>
@@ -75,12 +116,12 @@
<div> <div>
<label class="block text-sm font-medium text-gray-700">Status</label> <label class="block text-sm font-medium text-gray-700">Status</label>
<div class="mt-1"> <div class="mt-1">
<span :class="[ <span
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium', :class="[
apiKey.isActive 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
? 'bg-green-100 text-green-800' apiKey.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
: 'bg-red-100 text-red-800' ]"
]"> >
{{ apiKey.isActive ? 'Active' : 'Disabled' }} {{ apiKey.isActive ? 'Active' : 'Disabled' }}
</span> </span>
</div> </div>
@@ -104,7 +145,7 @@
<!-- Usage Stats --> <!-- Usage Stats -->
<div v-if="apiKey.usage" class="border-t border-gray-200 pt-4"> <div v-if="apiKey.usage" class="border-t border-gray-200 pt-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Usage Statistics</label> <label class="mb-2 block text-sm font-medium text-gray-700">Usage Statistics</label>
<div class="grid grid-cols-2 gap-4 text-sm"> <div class="grid grid-cols-2 gap-4 text-sm">
<div> <div>
<span class="text-gray-500">Requests:</span> <span class="text-gray-500">Requests:</span>
@@ -112,21 +153,27 @@
</div> </div>
<div> <div>
<span class="text-gray-500">Input Tokens:</span> <span class="text-gray-500">Input Tokens:</span>
<span class="ml-2 font-medium">{{ formatNumber(apiKey.usage.inputTokens || 0) }}</span> <span class="ml-2 font-medium">{{
formatNumber(apiKey.usage.inputTokens || 0)
}}</span>
</div> </div>
<div> <div>
<span class="text-gray-500">Output Tokens:</span> <span class="text-gray-500">Output Tokens:</span>
<span class="ml-2 font-medium">{{ formatNumber(apiKey.usage.outputTokens || 0) }}</span> <span class="ml-2 font-medium">{{
formatNumber(apiKey.usage.outputTokens || 0)
}}</span>
</div> </div>
<div> <div>
<span class="text-gray-500">Total Cost:</span> <span class="text-gray-500">Total Cost:</span>
<span class="ml-2 font-medium">${{ (apiKey.usage.totalCost || 0).toFixed(4) }}</span> <span class="ml-2 font-medium"
>${{ (apiKey.usage.totalCost || 0).toFixed(4) }}</span
>
</div> </div>
</div> </div>
</div> </div>
<!-- Timestamps --> <!-- Timestamps -->
<div class="border-t border-gray-200 pt-4 space-y-2 text-sm"> <div class="space-y-2 border-t border-gray-200 pt-4 text-sm">
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-gray-500">Created:</span> <span class="text-gray-500">Created:</span>
<span class="text-gray-900">{{ formatDate(apiKey.createdAt) }}</span> <span class="text-gray-900">{{ formatDate(apiKey.createdAt) }}</span>
@@ -137,10 +184,12 @@
</div> </div>
<div v-if="apiKey.expiresAt" class="flex justify-between"> <div v-if="apiKey.expiresAt" class="flex justify-between">
<span class="text-gray-500">Expires:</span> <span class="text-gray-500">Expires:</span>
<span :class="[ <span
:class="[
'font-medium', 'font-medium',
new Date(apiKey.expiresAt) < new Date() ? 'text-red-600' : 'text-gray-900' new Date(apiKey.expiresAt) < new Date() ? 'text-red-600' : 'text-gray-900'
]"> ]"
>
{{ formatDate(apiKey.expiresAt) }} {{ formatDate(apiKey.expiresAt) }}
</span> </span>
</div> </div>
@@ -148,30 +197,52 @@
<!-- Usage Instructions --> <!-- Usage Instructions -->
<div class="border-t border-gray-200 pt-4"> <div class="border-t border-gray-200 pt-4">
<label class="block text-sm font-medium text-gray-700 mb-2">Usage Instructions</label> <label class="mb-2 block text-sm font-medium text-gray-700">Usage Instructions</label>
<div class="bg-gray-50 p-3 rounded-md"> <div class="rounded-md bg-gray-50 p-3">
<p class="text-xs text-gray-600 mb-2">Set these environment variables to use this API key:</p> <p class="mb-2 text-xs text-gray-600">
<div class="space-y-1 text-xs font-mono"> Set these environment variables to use this API key:
</p>
<div class="space-y-1 font-mono text-xs">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<code class="text-gray-800">export ANTHROPIC_BASE_URL="http://your-server:3000/api/"</code> <code class="text-gray-800"
<button >export ANTHROPIC_BASE_URL="http://your-server:3000/api/"</code
@click="copyToClipboard('export ANTHROPIC_BASE_URL=\"http://your-server:3000/api/\"')"
class="ml-2 text-gray-400 hover:text-gray-600"
> >
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <button
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> class="ml-2 text-gray-400 hover:text-gray-600"
@click="
copyToClipboard(
`export ANTHROPIC_BASE_URL=&quot;http://your-server:3000/api/&quot;`
)
"
>
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</button> </button>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<code class="text-gray-800">export ANTHROPIC_AUTH_TOKEN="{{ apiKey.keyPreview || 'your-api-key' }}"</code> <code class="text-gray-800"
>export ANTHROPIC_AUTH_TOKEN="{{ apiKey.keyPreview || 'your-api-key' }}"</code
>
<button <button
v-if="apiKey.key" v-if="apiKey.key"
@click="copyToClipboard(`export ANTHROPIC_AUTH_TOKEN=\"${apiKey.key}\"`)"
class="ml-2 text-gray-400 hover:text-gray-600" class="ml-2 text-gray-400 hover:text-gray-600"
@click="
copyToClipboard(`export ANTHROPIC_AUTH_TOKEN=&quot;${apiKey.key}&quot;`)
"
> >
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> <path
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -181,8 +252,8 @@
<div class="flex justify-end pt-4"> <div class="flex justify-end pt-4">
<button <button
@click="$emit('close')" class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" @click="emit('close')"
> >
Close Close
</button> </button>
@@ -197,7 +268,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { showToast } from '@/utils/toast' import { showToast } from '@/utils/toast'
const props = defineProps({ defineProps({
show: { show: {
type: Boolean, type: Boolean,
default: false default: false

View File

@@ -2,13 +2,22 @@
<div class="min-h-screen bg-gray-50"> <div class="min-h-screen bg-gray-50">
<!-- 导航栏 --> <!-- 导航栏 -->
<nav class="bg-white shadow"> <nav class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16"> <div class="flex h-16 justify-between">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0 flex items-center"> <div class="flex flex-shrink-0 items-center">
<svg class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="h-8 w-8 text-blue-600"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
<span class="ml-2 text-xl font-bold text-gray-900">Claude Relay</span> <span class="ml-2 text-xl font-bold text-gray-900">Claude Relay</span>
</div> </div>
@@ -16,7 +25,7 @@
<div class="flex items-baseline space-x-4"> <div class="flex items-baseline space-x-4">
<button <button
:class="[ :class="[
'px-3 py-2 rounded-md text-sm font-medium', 'rounded-md px-3 py-2 text-sm font-medium',
activeTab === 'overview' activeTab === 'overview'
? 'bg-blue-100 text-blue-700' ? 'bg-blue-100 text-blue-700'
: 'text-gray-500 hover:text-gray-700' : 'text-gray-500 hover:text-gray-700'
@@ -27,7 +36,7 @@
</button> </button>
<button <button
:class="[ :class="[
'px-3 py-2 rounded-md text-sm font-medium', 'rounded-md px-3 py-2 text-sm font-medium',
activeTab === 'api-keys' activeTab === 'api-keys'
? 'bg-blue-100 text-blue-700' ? 'bg-blue-100 text-blue-700'
: 'text-gray-500 hover:text-gray-700' : 'text-gray-500 hover:text-gray-700'
@@ -38,7 +47,7 @@
</button> </button>
<button <button
:class="[ :class="[
'px-3 py-2 rounded-md text-sm font-medium', 'rounded-md px-3 py-2 text-sm font-medium',
activeTab === 'usage' activeTab === 'usage'
? 'bg-blue-100 text-blue-700' ? 'bg-blue-100 text-blue-700'
: 'text-gray-500 hover:text-gray-700' : 'text-gray-500 hover:text-gray-700'
@@ -55,8 +64,8 @@
Welcome, <span class="font-medium">{{ userStore.userName }}</span> Welcome, <span class="font-medium">{{ userStore.userName }}</span>
</div> </div>
<button <button
class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-700"
@click="handleLogout" @click="handleLogout"
class="text-gray-500 hover:text-gray-700 px-3 py-2 rounded-md text-sm font-medium"
> >
Logout Logout
</button> </button>
@@ -66,7 +75,7 @@
</nav> </nav>
<!-- 主内容 --> <!-- 主内容 -->
<main class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8"> <main class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<!-- Overview Tab --> <!-- Overview Tab -->
<div v-if="activeTab === 'overview'" class="space-y-6"> <div v-if="activeTab === 'overview'" class="space-y-6">
<div> <div>
@@ -75,73 +84,121 @@
</div> </div>
<!-- Stats Cards --> <!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m0 0a2 2 0 012 2m-2-2h-6m6 0v6a2 2 0 01-2 2H9a2 2 0 01-2-2V9a2 2 0 012-2h6z" /> class="h-6 w-6 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M15 7a2 2 0 012 2m0 0a2 2 0 012 2m-2-2h-6m6 0v6a2 2 0 01-2 2H9a2 2 0 01-2-2V9a2 2 0 012-2h6z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate">API Keys</dt> <dt class="truncate text-sm font-medium text-gray-500">API Keys</dt>
<dd class="text-lg font-medium text-gray-900">{{ userProfile?.apiKeyCount || 0 }}</dd> <dd class="text-lg font-medium text-gray-900">
{{ userProfile?.apiKeyCount || 0 }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> class="h-6 w-6 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M13 10V3L4 14h7v7l9-11h-7z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Requests</dt> <dt class="truncate text-sm font-medium text-gray-500">Total Requests</dt>
<dd class="text-lg font-medium text-gray-900">{{ formatNumber(userProfile?.totalUsage?.requests || 0) }}</dd> <dd class="text-lg font-medium text-gray-900">
{{ formatNumber(userProfile?.totalUsage?.requests || 0) }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> class="h-6 w-6 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate">Input Tokens</dt> <dt class="truncate text-sm font-medium text-gray-500">Input Tokens</dt>
<dd class="text-lg font-medium text-gray-900">{{ formatNumber(userProfile?.totalUsage?.inputTokens || 0) }}</dd> <dd class="text-lg font-medium text-gray-900">
{{ formatNumber(userProfile?.totalUsage?.inputTokens || 0) }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" /> class="h-6 w-6 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Cost</dt> <dt class="truncate text-sm font-medium text-gray-500">Total Cost</dt>
<dd class="text-lg font-medium text-gray-900">${{ (userProfile?.totalUsage?.totalCost || 0).toFixed(4) }}</dd> <dd class="text-lg font-medium text-gray-900">
${{ (userProfile?.totalUsage?.totalCost || 0).toFixed(4) }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
@@ -150,38 +207,50 @@
</div> </div>
<!-- User Info --> <!-- User Info -->
<div class="bg-white shadow rounded-lg"> <div class="rounded-lg bg-white shadow">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">Account Information</h3> <h3 class="text-lg font-medium leading-6 text-gray-900">Account Information</h3>
<div class="mt-5 border-t border-gray-200"> <div class="mt-5 border-t border-gray-200">
<dl class="divide-y divide-gray-200"> <dl class="divide-y divide-gray-200">
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4"> <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500">Username</dt> <dt class="text-sm font-medium text-gray-500">Username</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userProfile?.username }}</dd> <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ userProfile?.username }}
</dd>
</div> </div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4"> <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500">Display Name</dt> <dt class="text-sm font-medium text-gray-500">Display Name</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userProfile?.displayName || 'N/A' }}</dd> <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ userProfile?.displayName || 'N/A' }}
</dd>
</div> </div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4"> <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500">Email</dt> <dt class="text-sm font-medium text-gray-500">Email</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userProfile?.email || 'N/A' }}</dd> <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ userProfile?.email || 'N/A' }}
</dd>
</div> </div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4"> <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500">Role</dt> <dt class="text-sm font-medium text-gray-500">Role</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2"> <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> <span
class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800"
>
{{ userProfile?.role || 'user' }} {{ userProfile?.role || 'user' }}
</span> </span>
</dd> </dd>
</div> </div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4"> <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500">Member Since</dt> <dt class="text-sm font-medium text-gray-500">Member Since</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ formatDate(userProfile?.createdAt) }}</dd> <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ formatDate(userProfile?.createdAt) }}
</dd>
</div> </div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4"> <div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500">Last Login</dt> <dt class="text-sm font-medium text-gray-500">Last Login</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ formatDate(userProfile?.lastLoginAt) || 'N/A' }}</dd> <dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ formatDate(userProfile?.lastLoginAt) || 'N/A' }}
</dd>
</div> </div>
</dl> </dl>
</div> </div>

View File

@@ -1,65 +1,67 @@
<template> <template>
<div class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"> <div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8"> <div class="w-full max-w-md space-y-8">
<div> <div>
<div class="mx-auto h-12 w-auto flex items-center justify-center"> <div class="mx-auto flex h-12 w-auto items-center justify-center">
<svg class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-8 w-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
<span class="ml-2 text-xl font-bold text-gray-900">Claude Relay</span> <span class="ml-2 text-xl font-bold text-gray-900">Claude Relay</span>
</div> </div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900"> <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">User Sign In</h2>
User Sign In
</h2>
<p class="mt-2 text-center text-sm text-gray-600"> <p class="mt-2 text-center text-sm text-gray-600">
Sign in to your account to manage your API keys Sign in to your account to manage your API keys
</p> </p>
</div> </div>
<div class="bg-white rounded-lg shadow px-6 py-8"> <div class="rounded-lg bg-white px-6 py-8 shadow">
<form class="space-y-6" @submit.prevent="handleLogin"> <form class="space-y-6" @submit.prevent="handleLogin">
<div> <div>
<label for="username" class="block text-sm font-medium text-gray-700"> <label class="block text-sm font-medium text-gray-700" for="username"> Username </label>
Username
</label>
<div class="mt-1"> <div class="mt-1">
<input <input
id="username" id="username"
name="username"
type="text"
required
v-model="form.username" v-model="form.username"
class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-blue-500 focus:outline-none focus:ring-blue-500 sm:text-sm"
:disabled="loading" :disabled="loading"
class="appearance-none relative block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" name="username"
placeholder="Enter your username" placeholder="Enter your username"
required
type="text"
/> />
</div> </div>
</div> </div>
<div> <div>
<label for="password" class="block text-sm font-medium text-gray-700"> <label class="block text-sm font-medium text-gray-700" for="password"> Password </label>
Password
</label>
<div class="mt-1"> <div class="mt-1">
<input <input
id="password" id="password"
name="password"
type="password"
required
v-model="form.password" v-model="form.password"
class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-blue-500 focus:outline-none focus:ring-blue-500 sm:text-sm"
:disabled="loading" :disabled="loading"
class="appearance-none relative block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" name="password"
placeholder="Enter your password" placeholder="Enter your password"
required
type="password"
/> />
</div> </div>
</div> </div>
<div v-if="error" class="bg-red-50 border border-red-200 rounded-md p-4"> <div v-if="error" class="rounded-md border border-red-200 bg-red-50 p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> <path
clip-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
fill-rule="evenodd"
/>
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
@@ -70,14 +72,30 @@
<div> <div>
<button <button
type="submit" class="group relative flex w-full justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="loading || !form.username || !form.password" :disabled="loading || !form.username || !form.password"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" type="submit"
> >
<span v-if="loading" class="absolute left-0 inset-y-0 flex items-center pl-3"> <span v-if="loading" class="absolute inset-y-0 left-0 flex items-center pl-3">
<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> class="h-5 w-5 animate-spin text-white"
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
></path>
</svg> </svg>
</span> </span>
{{ loading ? 'Signing In...' : 'Sign In' }} {{ loading ? 'Signing In...' : 'Sign In' }}
@@ -85,10 +103,7 @@
</div> </div>
<div class="text-center"> <div class="text-center">
<router-link <router-link class="text-sm text-blue-600 hover:text-blue-500" to="/admin-login">
to="/admin-login"
class="text-sm text-blue-600 hover:text-blue-500"
>
Admin Login Admin Login
</router-link> </router-link>
</div> </div>

View File

@@ -8,14 +8,19 @@
Manage users, their API keys, and view usage statistics Manage users, their API keys, and view usage statistics
</p> </p>
</div> </div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"> <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<button <button
@click="loadUsers"
:disabled="loading"
class="inline-flex items-center justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 sm:w-auto" class="inline-flex items-center justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 sm:w-auto"
:disabled="loading"
@click="loadUsers"
> >
<svg class="-ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
Refresh Refresh
</button> </button>
@@ -23,18 +28,28 @@
</div> </div>
<!-- Stats Cards --> <!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" /> class="h-6 w-6 text-blue-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Users</dt> <dt class="truncate text-sm font-medium text-gray-500">Total Users</dt>
<dd class="text-lg font-medium text-gray-900">{{ userStats?.totalUsers || 0 }}</dd> <dd class="text-lg font-medium text-gray-900">{{ userStats?.totalUsers || 0 }}</dd>
</dl> </dl>
</div> </div>
@@ -42,17 +57,27 @@
</div> </div>
</div> </div>
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> class="h-6 w-6 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate">Active Users</dt> <dt class="truncate text-sm font-medium text-gray-500">Active Users</dt>
<dd class="text-lg font-medium text-gray-900">{{ userStats?.activeUsers || 0 }}</dd> <dd class="text-lg font-medium text-gray-900">{{ userStats?.activeUsers || 0 }}</dd>
</dl> </dl>
</div> </div>
@@ -60,36 +85,60 @@
</div> </div>
</div> </div>
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-purple-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m0 0a2 2 0 012 2m-2-2h-6m6 0v6a2 2 0 01-2 2H9a2 2 0 01-2-2V9a2 2 0 012-2h6z" /> class="h-6 w-6 text-purple-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M15 7a2 2 0 012 2m0 0a2 2 0 012 2m-2-2h-6m6 0v6a2 2 0 01-2 2H9a2 2 0 01-2-2V9a2 2 0 012-2h6z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total API Keys</dt> <dt class="truncate text-sm font-medium text-gray-500">Total API Keys</dt>
<dd class="text-lg font-medium text-gray-900">{{ userStats?.totalApiKeys || 0 }}</dd> <dd class="text-lg font-medium text-gray-900">
{{ userStats?.totalApiKeys || 0 }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" /> class="h-6 w-6 text-yellow-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Cost</dt> <dt class="truncate text-sm font-medium text-gray-500">Total Cost</dt>
<dd class="text-lg font-medium text-gray-900">${{ (userStats?.totalUsage?.totalCost || 0).toFixed(4) }}</dd> <dd class="text-lg font-medium text-gray-900">
${{ (userStats?.totalUsage?.totalCost || 0).toFixed(4) }}
</dd>
</dl> </dl>
</div> </div>
</div> </div>
@@ -98,24 +147,34 @@
</div> </div>
<!-- Search and Filters --> <!-- Search and Filters -->
<div class="bg-white shadow rounded-lg"> <div class="rounded-lg bg-white shadow">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-center sm:justify-between"> <div class="sm:flex sm:items-center sm:justify-between">
<div class="sm:flex sm:items-center space-y-4 sm:space-y-0 sm:space-x-4"> <div class="space-y-4 sm:flex sm:items-center sm:space-x-4 sm:space-y-0">
<!-- Search --> <!-- Search -->
<div class="flex-1 min-w-0"> <div class="min-w-0 flex-1">
<div class="relative rounded-md shadow-sm"> <div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> class="h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
<input <input
v-model="searchQuery" v-model="searchQuery"
@input="debouncedSearch" class="block w-full rounded-md border-gray-300 pl-10 focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
type="search"
class="focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md"
placeholder="Search users..." placeholder="Search users..."
type="search"
@input="debouncedSearch"
/> />
</div> </div>
</div> </div>
@@ -124,8 +183,8 @@
<div> <div>
<select <select
v-model="selectedRole" v-model="selectedRole"
@change="loadUsers"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
@change="loadUsers"
> >
<option value="">All Roles</option> <option value="">All Roles</option>
<option value="user">User</option> <option value="user">User</option>
@@ -137,8 +196,8 @@
<div> <div>
<select <select
v-model="selectedStatus" v-model="selectedStatus"
@change="loadUsers"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
@change="loadUsers"
> >
<option value="">All Status</option> <option value="">All Status</option>
<option value="true">Active</option> <option value="true">Active</option>
@@ -151,55 +210,85 @@
</div> </div>
<!-- Users Table --> <!-- Users Table -->
<div class="bg-white shadow overflow-hidden sm:rounded-md"> <div class="overflow-hidden bg-white shadow sm:rounded-md">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200"> <div class="border-b border-gray-200 px-4 py-5 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900"> <h3 class="text-lg font-medium leading-6 text-gray-900">
Users Users
<span v-if="!loading" class="text-sm text-gray-500">({{ filteredUsers.length }} of {{ users.length }})</span> <span v-if="!loading" class="text-sm text-gray-500"
>({{ filteredUsers.length }} of {{ users.length }})</span
>
</h3> </h3>
</div> </div>
<!-- Loading State --> <!-- Loading State -->
<div v-if="loading" class="text-center py-12"> <div v-if="loading" class="py-12 text-center">
<svg class="animate-spin h-8 w-8 text-blue-600 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> class="mx-auto h-8 w-8 animate-spin text-blue-600"
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
></path>
</svg> </svg>
<p class="mt-2 text-sm text-gray-500">Loading users...</p> <p class="mt-2 text-sm text-gray-500">Loading users...</p>
</div> </div>
<!-- Users List --> <!-- Users List -->
<ul v-else-if="filteredUsers.length > 0" role="list" class="divide-y divide-gray-200"> <ul v-else-if="filteredUsers.length > 0" class="divide-y divide-gray-200" role="list">
<li v-for="user in filteredUsers" :key="user.id" class="px-6 py-4"> <li v-for="user in filteredUsers" :key="user.id" class="px-6 py-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center min-w-0 flex-1"> <div class="flex min-w-0 flex-1 items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<div class="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center"> <div class="flex h-10 w-10 items-center justify-center rounded-full bg-gray-300">
<svg class="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> class="h-6 w-6 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</div> </div>
</div> </div>
<div class="ml-4 flex-1 min-w-0"> <div class="ml-4 min-w-0 flex-1">
<div class="flex items-center"> <div class="flex items-center">
<p class="text-sm font-medium text-gray-900 truncate"> <p class="truncate text-sm font-medium text-gray-900">
{{ user.displayName || user.username }} {{ user.displayName || user.username }}
</p> </p>
<div class="ml-2 flex items-center space-x-2"> <div class="ml-2 flex items-center space-x-2">
<span :class="[ <span
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium', :class="[
user.isActive 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
? 'bg-green-100 text-green-800' user.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
: 'bg-red-100 text-red-800' ]"
]"> >
{{ user.isActive ? 'Active' : 'Disabled' }} {{ user.isActive ? 'Active' : 'Disabled' }}
</span> </span>
<span :class="[ <span
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium', :class="[
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
user.role === 'admin' user.role === 'admin'
? 'bg-purple-100 text-purple-800' ? 'bg-purple-100 text-purple-800'
: 'bg-blue-100 text-blue-800' : 'bg-blue-100 text-blue-800'
]"> ]"
>
{{ user.role }} {{ user.role }}
</span> </span>
</div> </div>
@@ -208,10 +297,15 @@
<span>@{{ user.username }}</span> <span>@{{ user.username }}</span>
<span v-if="user.email">{{ user.email }}</span> <span v-if="user.email">{{ user.email }}</span>
<span>{{ user.apiKeyCount || 0 }} API keys</span> <span>{{ user.apiKeyCount || 0 }} API keys</span>
<span v-if="user.lastLoginAt">Last login: {{ formatDate(user.lastLoginAt) }}</span> <span v-if="user.lastLoginAt"
>Last login: {{ formatDate(user.lastLoginAt) }}</span
>
<span v-else>Never logged in</span> <span v-else>Never logged in</span>
</div> </div>
<div v-if="user.totalUsage" class="mt-1 flex items-center space-x-4 text-xs text-gray-400"> <div
v-if="user.totalUsage"
class="mt-1 flex items-center space-x-4 text-xs text-gray-400"
>
<span>{{ formatNumber(user.totalUsage.requests || 0) }} requests</span> <span>{{ formatNumber(user.totalUsage.requests || 0) }} requests</span>
<span>${{ (user.totalUsage.totalCost || 0).toFixed(4) }} total cost</span> <span>${{ (user.totalUsage.totalCost || 0).toFixed(4) }} total cost</span>
</div> </div>
@@ -220,54 +314,85 @@
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<!-- View Usage Stats --> <!-- View Usage Stats -->
<button <button
@click="viewUserStats(user)" class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-blue-600"
class="inline-flex items-center p-1 border border-transparent rounded text-gray-400 hover:text-blue-600"
title="View Usage Stats" title="View Usage Stats"
@click="viewUserStats(user)"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> <path
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</button> </button>
<!-- Disable User API Keys --> <!-- Disable User API Keys -->
<button <button
@click="disableUserApiKeys(user)" class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="user.apiKeyCount === 0" :disabled="user.apiKeyCount === 0"
class="inline-flex items-center p-1 border border-transparent rounded text-gray-400 hover:text-red-600 disabled:opacity-50 disabled:cursor-not-allowed"
title="Disable All API Keys" title="Disable All API Keys"
@click="disableUserApiKeys(user)"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 12M6 6l12 12" /> <path
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 12M6 6l12 12"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</button> </button>
<!-- Toggle User Status --> <!-- Toggle User Status -->
<button <button
@click="toggleUserStatus(user)"
:class="[ :class="[
'inline-flex items-center p-1 border border-transparent rounded', 'inline-flex items-center rounded border border-transparent p-1',
user.isActive user.isActive
? 'text-gray-400 hover:text-red-600' ? 'text-gray-400 hover:text-red-600'
: 'text-gray-400 hover:text-green-600' : 'text-gray-400 hover:text-green-600'
]" ]"
:title="user.isActive ? 'Disable User' : 'Enable User'" :title="user.isActive ? 'Disable User' : 'Enable User'"
@click="toggleUserStatus(user)"
> >
<svg v-if="user.isActive" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 12M6 6l12 12" /> v-if="user.isActive"
class="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 12M6 6l12 12"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
<svg v-else class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg v-else class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> <path
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</button> </button>
<!-- Change Role --> <!-- Change Role -->
<button <button
@click="changeUserRole(user)" class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-purple-600"
class="inline-flex items-center p-1 border border-transparent rounded text-gray-400 hover:text-purple-600"
title="Change Role" title="Change Role"
@click="changeUserRole(user)"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" /> <path
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -276,13 +401,25 @@
</ul> </ul>
<!-- Empty State --> <!-- Empty State -->
<div v-else class="text-center py-12"> <div v-else class="py-12 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" /> class="mx-auto h-12 w-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg> </svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No users found</h3> <h3 class="mt-2 text-sm font-medium text-gray-900">No users found</h3>
<p class="mt-1 text-sm text-gray-500"> <p class="mt-1 text-sm text-gray-500">
{{ searchQuery ? 'No users match your search criteria.' : 'No users have been created yet.' }} {{
searchQuery ? 'No users match your search criteria.' : 'No users have been created yet.'
}}
</p> </p>
</div> </div>
</div> </div>
@@ -296,13 +433,13 @@
<!-- Confirm Modals --> <!-- Confirm Modals -->
<ConfirmModal <ConfirmModal
:confirm-class="confirmAction.confirmClass"
:confirm-text="confirmAction.confirmText"
:message="confirmAction.message"
:show="showConfirmModal" :show="showConfirmModal"
:title="confirmAction.title" :title="confirmAction.title"
:message="confirmAction.message"
:confirmText="confirmAction.confirmText"
:confirmClass="confirmAction.confirmClass"
@confirm="handleConfirmAction"
@cancel="showConfirmModal = false" @cancel="showConfirmModal = false"
@confirm="handleConfirmAction"
/> />
<!-- Change Role Modal --> <!-- Change Role Modal -->
@@ -350,7 +487,8 @@ const filteredUsers = computed(() => {
// Apply search filter // Apply search filter
if (searchQuery.value) { if (searchQuery.value) {
const query = searchQuery.value.toLowerCase() const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(user => filtered = filtered.filter(
(user) =>
user.username.toLowerCase().includes(query) || user.username.toLowerCase().includes(query) ||
user.displayName?.toLowerCase().includes(query) || user.displayName?.toLowerCase().includes(query) ||
user.email?.toLowerCase().includes(query) user.email?.toLowerCase().includes(query)
@@ -359,13 +497,13 @@ const filteredUsers = computed(() => {
// Apply role filter // Apply role filter
if (selectedRole.value) { if (selectedRole.value) {
filtered = filtered.filter(user => user.role === selectedRole.value) filtered = filtered.filter((user) => user.role === selectedRole.value)
} }
// Apply status filter // Apply status filter
if (selectedStatus.value !== '') { if (selectedStatus.value !== '') {
const isActive = selectedStatus.value === 'true' const isActive = selectedStatus.value === 'true'
filtered = filtered.filter(user => user.isActive === isActive) filtered = filtered.filter((user) => user.isActive === isActive)
} }
return filtered return filtered
@@ -472,7 +610,7 @@ const handleConfirmAction = async () => {
}) })
if (response.data.success) { if (response.data.success) {
const userIndex = users.value.findIndex(u => u.id === user.id) const userIndex = users.value.findIndex((u) => u.id === user.id)
if (userIndex !== -1) { if (userIndex !== -1) {
users.value[userIndex].isActive = !user.isActive users.value[userIndex].isActive = !user.isActive
} }