| <script setup lang="ts"> |
| import { computed } from 'vue' |
| import { useI18n } from 'vue-i18n' |
| import { Chart as ChartJS, BarElement, CategoryScale, Legend, LinearScale, Tooltip } from 'chart.js' |
| import { Bar } from 'vue-chartjs' |
| import type { OpsLatencyHistogramResponse } from '@/api/admin/ops' |
| import type { ChartState } from '../types' |
| import HelpTooltip from '@/components/common/HelpTooltip.vue' |
| import EmptyState from '@/components/common/EmptyState.vue' |
|
|
| ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend) |
|
|
| interface Props { |
| latencyData: OpsLatencyHistogramResponse | null |
| loading: boolean |
| } |
|
|
| const props = defineProps<Props>() |
| const { t } = useI18n() |
|
|
| const isDarkMode = computed(() => document.documentElement.classList.contains('dark')) |
| const colors = computed(() => ({ |
| blue: '#3b82f6', |
| grid: isDarkMode.value ? '#374151' : '#f3f4f6', |
| text: isDarkMode.value ? '#9ca3af' : '#6b7280' |
| })) |
|
|
| const hasData = computed(() => (props.latencyData?.total_requests ?? 0) > 0) |
|
|
| const state = computed<ChartState>(() => { |
| if (hasData.value) return 'ready' |
| if (props.loading) return 'loading' |
| return 'empty' |
| }) |
|
|
| const chartData = computed(() => { |
| if (!props.latencyData || !hasData.value) return null |
| const c = colors.value |
| return { |
| labels: props.latencyData.buckets.map((b) => b.range), |
| datasets: [ |
| { |
| label: t('admin.ops.requests'), |
| data: props.latencyData.buckets.map((b) => b.count), |
| backgroundColor: c.blue, |
| borderRadius: 4, |
| barPercentage: 0.6 |
| } |
| ] |
| } |
| }) |
|
|
| const options = computed(() => { |
| const c = colors.value |
| return { |
| responsive: true, |
| maintainAspectRatio: false, |
| plugins: { |
| legend: { display: false } |
| }, |
| scales: { |
| x: { |
| grid: { display: false }, |
| ticks: { color: c.text, font: { size: 10 } } |
| }, |
| y: { |
| beginAtZero: true, |
| grid: { color: c.grid, borderDash: [4, 4] }, |
| ticks: { color: c.text, font: { size: 10 } } |
| } |
| } |
| } |
| }) |
| </script> |
|
|
| <template> |
| <div class="flex h-full flex-col rounded-3xl bg-white p-6 shadow-sm ring-1 ring-gray-900/5 dark:bg-dark-800 dark:ring-dark-700"> |
| <div class="mb-4 flex items-center justify-between"> |
| <h3 class="flex items-center gap-2 text-sm font-bold text-gray-900 dark:text-white"> |
| <svg class="h-4 w-4 text-purple-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| <path |
| stroke-linecap="round" |
| stroke-linejoin="round" |
| stroke-width="2" |
| d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" |
| /> |
| </svg> |
| {{ t('admin.ops.latencyHistogram') }} |
| <HelpTooltip :content="t('admin.ops.tooltips.latencyHistogram')" /> |
| </h3> |
| </div> |
|
|
| <div class="min-h-0 flex-1"> |
| <Bar v-if="state === 'ready' && chartData" :data="chartData" :options="options" /> |
| <div v-else class="flex h-full items-center justify-center"> |
| <div v-if="state === 'loading'" class="animate-pulse text-sm text-gray-400">{{ t('common.loading') }}</div> |
| <EmptyState v-else :title="t('common.noData')" :description="t('admin.ops.charts.emptyRequest')" /> |
| </div> |
| </div> |
| </div> |
| </template> |
|
|