| <template> |
| <Teleport to="body"> |
| <div v-if="show && position"> |
| |
| <div class="fixed inset-0 z-[9998]" @click="emit('close')"></div> |
| <div |
| class="action-menu-content fixed z-[9999] w-52 overflow-hidden rounded-xl bg-white shadow-lg ring-1 ring-black/5 dark:bg-dark-800" |
| :style="{ top: position.top + 'px', left: position.left + 'px' }" |
| @click.stop |
| > |
| <div class="py-1"> |
| <template v-if="account"> |
| <button @click="$emit('test', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-dark-700"> |
| <Icon name="play" size="sm" class="text-green-500" :stroke-width="2" /> |
| {{ t('admin.accounts.testConnection') }} |
| </button> |
| <button @click="$emit('stats', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-dark-700"> |
| <Icon name="chart" size="sm" class="text-indigo-500" /> |
| {{ t('admin.accounts.viewStats') }} |
| </button> |
| <button @click="$emit('schedule', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-dark-700"> |
| <Icon name="clock" size="sm" class="text-orange-500" /> |
| {{ t('admin.scheduledTests.schedule') }} |
| </button> |
| <template v-if="account.type === 'oauth' || account.type === 'setup-token'"> |
| <button @click="$emit('reauth', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-blue-600 hover:bg-gray-100 dark:hover:bg-dark-700"> |
| <Icon name="link" size="sm" /> |
| {{ t('admin.accounts.reAuthorize') }} |
| </button> |
| <button @click="$emit('refresh-token', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-purple-600 hover:bg-gray-100 dark:hover:bg-dark-700"> |
| <Icon name="refresh" size="sm" /> |
| {{ t('admin.accounts.refreshToken') }} |
| </button> |
| </template> |
| <div v-if="hasRecoverableState" class="my-1 border-t border-gray-100 dark:border-dark-700"></div> |
| <button v-if="hasRecoverableState" @click="$emit('recover-state', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-emerald-600 hover:bg-gray-100 dark:hover:bg-dark-700"> |
| <Icon name="sync" size="sm" /> |
| {{ t('admin.accounts.recoverState') }} |
| </button> |
| <button v-if="hasQuotaLimit" @click="$emit('reset-quota', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-teal-600 hover:bg-gray-100 dark:hover:bg-dark-700"> |
| <Icon name="refresh" size="sm" /> |
| {{ t('admin.accounts.resetQuota') }} |
| </button> |
| </template> |
| </div> |
| </div> |
| </div> |
| </Teleport> |
| </template> |
| |
| <script setup lang="ts"> |
| import { computed, watch, onUnmounted } from 'vue' |
| import { useI18n } from 'vue-i18n' |
| import { Icon } from '@/components/icons' |
| import type { Account } from '@/types' |
| |
| const props = defineProps<{ show: boolean; account: Account | null; position: { top: number; left: number } | null }>() |
| const emit = defineEmits(['close', 'test', 'stats', 'schedule', 'reauth', 'refresh-token', 'recover-state', 'reset-quota']) |
| const { t } = useI18n() |
| const isRateLimited = computed(() => { |
| if (props.account?.rate_limit_reset_at && new Date(props.account.rate_limit_reset_at) > new Date()) { |
| return true |
| } |
| const modelLimits = (props.account?.extra as Record<string, unknown> | undefined)?.model_rate_limits as |
| | Record<string, { rate_limit_reset_at: string }> |
| | undefined |
| if (modelLimits) { |
| const now = new Date() |
| return Object.values(modelLimits).some(info => new Date(info.rate_limit_reset_at) > now) |
| } |
| return false |
| }) |
| const isOverloaded = computed(() => props.account?.overload_until && new Date(props.account.overload_until) > new Date()) |
| const isTempUnschedulable = computed(() => props.account?.temp_unschedulable_until && new Date(props.account.temp_unschedulable_until) > new Date()) |
| const hasRecoverableState = computed(() => { |
| return props.account?.status === 'error' || Boolean(isRateLimited.value) || Boolean(isOverloaded.value) || Boolean(isTempUnschedulable.value) |
| }) |
| const hasQuotaLimit = computed(() => { |
| return (props.account?.type === 'apikey' || props.account?.type === 'bedrock') && ( |
| (props.account?.quota_limit ?? 0) > 0 || |
| (props.account?.quota_daily_limit ?? 0) > 0 || |
| (props.account?.quota_weekly_limit ?? 0) > 0 |
| ) |
| }) |
| |
| const handleKeydown = (event: KeyboardEvent) => { |
| if (event.key === 'Escape') emit('close') |
| } |
| |
| watch( |
| () => props.show, |
| (visible) => { |
| if (visible) { |
| window.addEventListener('keydown', handleKeydown) |
| } else { |
| window.removeEventListener('keydown', handleKeydown) |
| } |
| }, |
| { immediate: true } |
| ) |
| |
| onUnmounted(() => { |
| window.removeEventListener('keydown', handleKeydown) |
| }) |
| </script> |
| |