feat: add admin provider analytics dashboard
This commit is contained in:
@@ -29,6 +29,248 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<BaseCard padding="lg">
|
||||
<div class="flex flex-col gap-5 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="max-w-2xl">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<h2 class="text-xl font-bold text-gray-900">当前环境 Provider 运营摘要</h2>
|
||||
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700">
|
||||
跨用户 / 当前环境
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-2 text-sm leading-6 text-gray-500">
|
||||
这里展示的是当前部署环境内所有生成任务留下的 Provider 调用轨迹,便于运营和排障。
|
||||
跨环境对比仍需要后续独立汇聚层。
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
||||
:class="analyticsWindow === '7' ? 'border-gray-900 bg-gray-900 text-white' : 'border-gray-200 bg-white text-gray-600 hover:border-gray-400'"
|
||||
@click="analyticsWindow = '7'"
|
||||
>
|
||||
最近 7 天
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
||||
:class="analyticsWindow === '30' ? 'border-gray-900 bg-gray-900 text-white' : 'border-gray-200 bg-white text-gray-600 hover:border-gray-400'"
|
||||
@click="analyticsWindow = '30'"
|
||||
>
|
||||
最近 30 天
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
||||
:class="analyticsWindow === 'all' ? 'border-gray-900 bg-gray-900 text-white' : 'border-gray-200 bg-white text-gray-600 hover:border-gray-400'"
|
||||
@click="analyticsWindow = 'all'"
|
||||
>
|
||||
全部
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
||||
:class="analyticsCapability === 'all' ? 'border-indigo-600 bg-indigo-600 text-white' : 'border-gray-200 bg-white text-gray-600 hover:border-gray-400'"
|
||||
@click="analyticsCapability = 'all'"
|
||||
>
|
||||
全部能力
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
||||
:class="analyticsCapability === 'text' ? 'border-indigo-600 bg-indigo-600 text-white' : 'border-gray-200 bg-white text-gray-600 hover:border-gray-400'"
|
||||
@click="analyticsCapability = 'text'"
|
||||
>
|
||||
文本
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
||||
:class="analyticsCapability === 'image' ? 'border-indigo-600 bg-indigo-600 text-white' : 'border-gray-200 bg-white text-gray-600 hover:border-gray-400'"
|
||||
@click="analyticsCapability = 'image'"
|
||||
>
|
||||
图片
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
||||
:class="analyticsCapability === 'tts' ? 'border-indigo-600 bg-indigo-600 text-white' : 'border-gray-200 bg-white text-gray-600 hover:border-gray-400'"
|
||||
@click="analyticsCapability = 'tts'"
|
||||
>
|
||||
语音
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
||||
:class="analyticsCapability === 'storybook' ? 'border-indigo-600 bg-indigo-600 text-white' : 'border-gray-200 bg-white text-gray-600 hover:border-gray-400'"
|
||||
@click="analyticsCapability = 'storybook'"
|
||||
>
|
||||
绘本
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4 xl:min-w-[420px]">
|
||||
<div class="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<div class="text-xs text-gray-500">活跃用户</div>
|
||||
<div class="mt-1 text-2xl font-semibold text-gray-900">
|
||||
{{ analytics?.user_count ?? 0 }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<div class="text-xs text-gray-500">总调用</div>
|
||||
<div class="mt-1 text-2xl font-semibold text-gray-900">
|
||||
{{ analytics?.total_calls ?? 0 }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<div class="text-xs text-gray-500">成功率</div>
|
||||
<div class="mt-1 text-2xl font-semibold text-gray-900">
|
||||
{{ providerSuccessRate ?? '--' }}<span v-if="providerSuccessRate !== null">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<div class="text-xs text-gray-500">预估成本</div>
|
||||
<div class="mt-1 text-2xl font-semibold text-gray-900">
|
||||
{{ formatCost(analytics?.estimated_cost_usd) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="analyticsLoading" class="mt-5 rounded-xl border border-dashed border-gray-200 px-4 py-6 text-sm text-gray-500">
|
||||
正在更新运营摘要...
|
||||
</div>
|
||||
<div v-else-if="analyticsError" class="mt-5 rounded-xl border border-rose-100 bg-rose-50 px-4 py-4 text-sm text-rose-600">
|
||||
{{ analyticsError }}
|
||||
</div>
|
||||
<template v-else-if="analytics">
|
||||
<div class="mt-6 grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<div class="rounded-xl border border-gray-100 bg-white px-4 py-3">
|
||||
<div class="text-xs text-gray-500">覆盖故事</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900">{{ analytics.story_count }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-100 bg-white px-4 py-3">
|
||||
<div class="text-xs text-gray-500">覆盖任务</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900">{{ analytics.job_count }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-100 bg-white px-4 py-3">
|
||||
<div class="text-xs text-gray-500">平均耗时</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900">{{ formatLatency(analytics.avg_latency_ms) }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-100 bg-white px-4 py-3">
|
||||
<div class="text-xs text-gray-500">配置中 Provider</div>
|
||||
<div class="mt-1 text-lg font-semibold text-gray-900">{{ enabledProviderCount }}/{{ providers.length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
|
||||
<div class="rounded-2xl border border-gray-100 bg-white">
|
||||
<div class="flex items-center justify-between border-b border-gray-100 px-5 py-4">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900">Provider 调用明细</h3>
|
||||
<p class="mt-1 text-xs text-gray-500">按能力和 adapter 聚合的当前环境视图</p>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400">{{ analyticsProviderRows.length }} 个组合</span>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100">
|
||||
<div
|
||||
v-for="row in analyticsProviderRows"
|
||||
:key="`${row.capability}:${row.adapter}`"
|
||||
class="grid grid-cols-1 gap-3 px-5 py-4 md:grid-cols-[minmax(0,1.2fr)_repeat(4,minmax(0,0.8fr))]"
|
||||
>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">
|
||||
{{ formatCapability(row.capability) }}
|
||||
</span>
|
||||
<span class="font-medium text-gray-900">{{ row.adapter }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-gray-400">调用</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-900">{{ row.call_count }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-gray-400">成功率</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-900">
|
||||
{{ getSuccessRate(row.success_count, row.call_count) ?? '--' }}<span v-if="getSuccessRate(row.success_count, row.call_count) !== null">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-gray-400">耗时</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-900">{{ formatLatency(row.avg_latency_ms) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-gray-400">成本</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-900">{{ formatCost(row.estimated_cost_usd) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="analyticsProviderRows.length === 0" class="px-5 py-8 text-sm text-gray-500">
|
||||
当前筛选条件下还没有 Provider 调用样本。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="rounded-2xl border border-gray-100 bg-white">
|
||||
<div class="border-b border-gray-100 px-5 py-4">
|
||||
<h3 class="font-semibold text-gray-900">跨用户分布</h3>
|
||||
<p class="mt-1 text-xs text-gray-500">按调用量排序,帮助快速识别主要使用者</p>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100">
|
||||
<div
|
||||
v-for="row in analyticsUserRows.slice(0, 5)"
|
||||
:key="row.user_id"
|
||||
class="px-5 py-4"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900">{{ row.user_id }}</div>
|
||||
<div class="mt-1 text-xs text-gray-500">
|
||||
{{ row.story_count }} 个故事 · {{ row.job_count }} 个任务
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-sm font-semibold text-gray-900">{{ row.call_count }} 次</div>
|
||||
<div class="mt-1 text-xs text-gray-500">{{ formatCost(row.estimated_cost_usd) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="analyticsUserRows.length === 0" class="px-5 py-8 text-sm text-gray-500">
|
||||
当前还没有跨用户样本。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-gray-100 bg-white">
|
||||
<div class="border-b border-gray-100 px-5 py-4">
|
||||
<h3 class="font-semibold text-gray-900">最近失败原因</h3>
|
||||
<p class="mt-1 text-xs text-gray-500">先看最常见原因,再决定是否调整路由或优先级</p>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100">
|
||||
<div
|
||||
v-for="reason in analytics.failure_reasons.slice(0, 5)"
|
||||
:key="reason.reason"
|
||||
class="flex items-center justify-between gap-3 px-5 py-4"
|
||||
>
|
||||
<span class="text-sm text-gray-700">{{ reason.reason }}</span>
|
||||
<span class="rounded-full bg-rose-50 px-2.5 py-1 text-xs font-medium text-rose-600">
|
||||
{{ reason.count }} 次
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="analytics.failure_reasons.length === 0" class="px-5 py-8 text-sm text-gray-500">
|
||||
当前窗口内还没有失败调用。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BaseCard>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||
|
||||
@@ -275,7 +517,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import BaseButton from '../components/ui/BaseButton.vue'
|
||||
import BaseCard from '../components/ui/BaseCard.vue'
|
||||
import BaseInput from '../components/ui/BaseInput.vue'
|
||||
@@ -299,7 +541,46 @@ type Provider = {
|
||||
config_json: Record<string, any>
|
||||
}
|
||||
|
||||
// State
|
||||
type ProviderAnalyticsBucket = {
|
||||
capability: string
|
||||
adapter: string
|
||||
call_count: number
|
||||
success_count: number
|
||||
failure_count: number
|
||||
avg_latency_ms: number | null
|
||||
estimated_cost_usd: number
|
||||
}
|
||||
|
||||
type ProviderAnalyticsUserBucket = {
|
||||
user_id: string
|
||||
call_count: number
|
||||
success_count: number
|
||||
failure_count: number
|
||||
job_count: number
|
||||
story_count: number
|
||||
estimated_cost_usd: number
|
||||
}
|
||||
|
||||
type ProviderAnalyticsResponse = {
|
||||
scope: string
|
||||
window_days: number | null
|
||||
capability: string | null
|
||||
total_calls: number
|
||||
successful_calls: number
|
||||
failed_calls: number
|
||||
avg_latency_ms: number | null
|
||||
estimated_cost_usd: number
|
||||
user_count: number
|
||||
job_count: number
|
||||
story_count: number
|
||||
by_provider: ProviderAnalyticsBucket[]
|
||||
by_user: ProviderAnalyticsUserBucket[]
|
||||
failure_reasons: Array<{
|
||||
reason: string
|
||||
count: number
|
||||
}>
|
||||
}
|
||||
|
||||
// State
|
||||
const loginForm = ref({ username: '', password: '' })
|
||||
const loginError = ref('')
|
||||
@@ -308,6 +589,11 @@ const activeTab = ref('text')
|
||||
const providers = ref<Provider[]>([])
|
||||
const defaults = ref<Record<string, string[]>>({})
|
||||
const availableAdapters = ref<string[]>([])
|
||||
const analytics = ref<ProviderAnalyticsResponse | null>(null)
|
||||
const analyticsLoading = ref(false)
|
||||
const analyticsError = ref('')
|
||||
const analyticsWindow = ref<'7' | '30' | 'all'>('30')
|
||||
const analyticsCapability = ref<'all' | 'text' | 'image' | 'tts' | 'storybook'>('all')
|
||||
const editing = ref(false)
|
||||
const form = ref<Partial<Provider> & { api_key?: string; config_json: Record<string, any> }>({
|
||||
type: 'text',
|
||||
@@ -322,6 +608,46 @@ function getAuthHeader(): string {
|
||||
return sessionStorage.getItem('admin_auth') || ''
|
||||
}
|
||||
|
||||
function buildAnalyticsPath() {
|
||||
const params = new URLSearchParams()
|
||||
if (analyticsWindow.value !== 'all') {
|
||||
params.set('days', analyticsWindow.value)
|
||||
}
|
||||
if (analyticsCapability.value !== 'all') {
|
||||
params.set('capability', analyticsCapability.value)
|
||||
}
|
||||
const query = params.toString()
|
||||
return `${apiBase}/admin/providers/analytics${query ? `?${query}` : ''}`
|
||||
}
|
||||
|
||||
function formatLatency(value?: number | null) {
|
||||
return typeof value === 'number' ? `${Math.round(value)}ms` : '暂无'
|
||||
}
|
||||
|
||||
function formatCost(value?: number | null) {
|
||||
return typeof value === 'number' ? `$${value.toFixed(4)}` : '$0.0000'
|
||||
}
|
||||
|
||||
function formatCapability(value: string) {
|
||||
switch (value) {
|
||||
case 'text':
|
||||
return '文本'
|
||||
case 'image':
|
||||
return '图片'
|
||||
case 'tts':
|
||||
return '语音'
|
||||
case 'storybook':
|
||||
return '绘本'
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function getSuccessRate(successCount: number, totalCount: number) {
|
||||
if (!totalCount) return null
|
||||
return Math.round((successCount / totalCount) * 100)
|
||||
}
|
||||
|
||||
// Actions
|
||||
async function login() {
|
||||
loginError.value = ''
|
||||
@@ -349,24 +675,58 @@ function logout() {
|
||||
sessionStorage.removeItem('admin_auth')
|
||||
isLoggedIn.value = false
|
||||
providers.value = []
|
||||
analytics.value = null
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
if (!isLoggedIn.value) return
|
||||
|
||||
const headers = { Authorization: getAuthHeader() }
|
||||
|
||||
// Parallel fetch
|
||||
const [pRes, dRes, aRes] = await Promise.all([
|
||||
fetch(`${apiBase}/admin/providers`, { headers }),
|
||||
fetch(`${apiBase}/admin/providers/defaults`, { headers }),
|
||||
fetch(`${apiBase}/admin/providers/adapters`, { headers })
|
||||
])
|
||||
async function loadAnalytics() {
|
||||
if (!isLoggedIn.value) return
|
||||
|
||||
if (pRes.ok) providers.value = await pRes.json()
|
||||
if (dRes.ok) defaults.value = await dRes.json()
|
||||
if (aRes.ok) availableAdapters.value = await aRes.json()
|
||||
analyticsLoading.value = true
|
||||
analyticsError.value = ''
|
||||
try {
|
||||
const response = await fetch(buildAnalyticsPath(), {
|
||||
headers: { Authorization: getAuthHeader() },
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('运营数据加载失败')
|
||||
}
|
||||
analytics.value = await response.json()
|
||||
} catch (error) {
|
||||
analyticsError.value = error instanceof Error ? error.message : '运营数据加载失败'
|
||||
analytics.value = null
|
||||
} finally {
|
||||
analyticsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
if (!isLoggedIn.value) return
|
||||
|
||||
const headers = { Authorization: getAuthHeader() }
|
||||
analyticsLoading.value = true
|
||||
|
||||
try {
|
||||
const [pRes, dRes, aRes, analyticsRes] = await Promise.all([
|
||||
fetch(`${apiBase}/admin/providers`, { headers }),
|
||||
fetch(`${apiBase}/admin/providers/defaults`, { headers }),
|
||||
fetch(`${apiBase}/admin/providers/adapters`, { headers }),
|
||||
fetch(buildAnalyticsPath(), { headers }),
|
||||
])
|
||||
|
||||
if (pRes.ok) providers.value = await pRes.json()
|
||||
if (dRes.ok) defaults.value = await dRes.json()
|
||||
if (aRes.ok) availableAdapters.value = await aRes.json()
|
||||
if (analyticsRes.ok) {
|
||||
analytics.value = await analyticsRes.json()
|
||||
analyticsError.value = ''
|
||||
} else {
|
||||
analytics.value = null
|
||||
analyticsError.value = '运营数据加载失败'
|
||||
}
|
||||
} finally {
|
||||
analyticsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Computed
|
||||
@@ -385,6 +745,37 @@ const adapterOptions = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const enabledProviderCount = computed(() => providers.value.filter((provider) => provider.enabled).length)
|
||||
|
||||
const providerSuccessRate = computed(() => {
|
||||
if (!analytics.value?.total_calls) return null
|
||||
return getSuccessRate(analytics.value.successful_calls, analytics.value.total_calls)
|
||||
})
|
||||
|
||||
const analyticsProviderRows = computed(() => {
|
||||
return [...(analytics.value?.by_provider || [])].sort((left, right) => {
|
||||
if (right.call_count !== left.call_count) {
|
||||
return right.call_count - left.call_count
|
||||
}
|
||||
if (right.estimated_cost_usd !== left.estimated_cost_usd) {
|
||||
return right.estimated_cost_usd - left.estimated_cost_usd
|
||||
}
|
||||
return left.adapter.localeCompare(right.adapter)
|
||||
})
|
||||
})
|
||||
|
||||
const analyticsUserRows = computed(() => {
|
||||
return [...(analytics.value?.by_user || [])].sort((left, right) => {
|
||||
if (right.call_count !== left.call_count) {
|
||||
return right.call_count - left.call_count
|
||||
}
|
||||
if (right.estimated_cost_usd !== left.estimated_cost_usd) {
|
||||
return right.estimated_cost_usd - left.estimated_cost_usd
|
||||
}
|
||||
return left.user_id.localeCompare(right.user_id)
|
||||
})
|
||||
})
|
||||
|
||||
// UI Actions
|
||||
function cloneDefault(type: string, name: string) {
|
||||
// 根据默认名称推断配置
|
||||
@@ -431,28 +822,28 @@ function reset() {
|
||||
|
||||
async function submit() {
|
||||
const method = form.value.id ? 'PUT' : 'POST'
|
||||
const url = form.value.id
|
||||
const url = form.value.id
|
||||
? `${apiBase}/admin/providers/${form.value.id}`
|
||||
: `${apiBase}/admin/providers`
|
||||
|
||||
|
||||
await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: getAuthHeader()
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
body: JSON.stringify(form.value)
|
||||
body: JSON.stringify(form.value),
|
||||
})
|
||||
|
||||
|
||||
await loadData()
|
||||
reset()
|
||||
}
|
||||
|
||||
async function remove(p: Provider) {
|
||||
if(!confirm(`确认删除 ${p.name}?`)) return
|
||||
if (!confirm(`确认删除 ${p.name}?`)) return
|
||||
await fetch(`${apiBase}/admin/providers/${p.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: getAuthHeader() }
|
||||
headers: { Authorization: getAuthHeader() },
|
||||
})
|
||||
await loadData()
|
||||
}
|
||||
@@ -460,11 +851,11 @@ async function remove(p: Provider) {
|
||||
async function toggleEnabled(p: Provider) {
|
||||
await fetch(`${apiBase}/admin/providers/${p.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: getAuthHeader()
|
||||
Authorization: getAuthHeader(),
|
||||
},
|
||||
body: JSON.stringify({ enabled: !p.enabled })
|
||||
body: JSON.stringify({ enabled: !p.enabled }),
|
||||
})
|
||||
await loadData()
|
||||
}
|
||||
@@ -472,4 +863,10 @@ async function toggleEnabled(p: Provider) {
|
||||
onMounted(() => {
|
||||
if (isLoggedIn.value) loadData()
|
||||
})
|
||||
|
||||
watch([analyticsWindow, analyticsCapability], () => {
|
||||
if (isLoggedIn.value) {
|
||||
void loadAnalytics()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user