mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-01 08:28:34 +00:00
Merge branch 'main' into fix/image-tier-validation
This commit is contained in:
@@ -9012,14 +9012,6 @@ watch(
|
||||
0 1px 0 rgb(255 255 255 / 0.9) inset;
|
||||
}
|
||||
|
||||
:global(.dark) .settings-tabs-shell {
|
||||
border-color: rgb(51 65 85 / 0.65);
|
||||
background: rgb(15 23 42 / 0.86);
|
||||
box-shadow:
|
||||
0 16px 36px rgb(0 0 0 / 0.28),
|
||||
0 1px 0 rgb(255 255 255 / 0.06) inset;
|
||||
}
|
||||
|
||||
.settings-tabs-scroll {
|
||||
@apply overflow-x-auto;
|
||||
-ms-overflow-style: none;
|
||||
@@ -9063,10 +9055,6 @@ watch(
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:global(.dark) .settings-tab::before {
|
||||
background: linear-gradient(135deg, rgb(30 41 59 / 0.9), rgb(51 65 85 / 0.62));
|
||||
}
|
||||
|
||||
.settings-tab:focus-visible {
|
||||
@apply ring-2 ring-primary-500/40 ring-offset-2 ring-offset-white dark:ring-offset-dark-900;
|
||||
}
|
||||
@@ -9078,12 +9066,6 @@ watch(
|
||||
0 1px 0 rgb(255 255 255 / 0.92) inset;
|
||||
}
|
||||
|
||||
:global(.dark) .settings-tab-active {
|
||||
box-shadow:
|
||||
0 12px 26px rgb(0 0 0 / 0.22),
|
||||
0 1px 0 rgb(255 255 255 / 0.08) inset;
|
||||
}
|
||||
|
||||
.settings-tab-active::before {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -9116,3 +9098,26 @@ watch(
|
||||
@apply min-w-0 overflow-hidden text-ellipsis whitespace-nowrap leading-none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Dark-mode overrides for the settings tabs shell. Kept in an UNSCOPED block
|
||||
because Vue's scoped-CSS compiler was dropping the `:global(.dark) ...`
|
||||
rules in the production build, leaving inactive tabs unreadable on dark. */
|
||||
.dark .settings-tabs-shell {
|
||||
border-color: rgb(51 65 85 / 0.65);
|
||||
background: rgb(15 23 42 / 0.86);
|
||||
box-shadow:
|
||||
0 16px 36px rgb(0 0 0 / 0.28),
|
||||
0 1px 0 rgb(255 255 255 / 0.06) inset;
|
||||
}
|
||||
|
||||
.dark .settings-tab::before {
|
||||
background: linear-gradient(135deg, rgb(30 41 59 / 0.9), rgb(51 65 85 / 0.62));
|
||||
}
|
||||
|
||||
.dark .settings-tab-active {
|
||||
box-shadow:
|
||||
0 12px 26px rgb(0 0 0 / 0.22),
|
||||
0 1px 0 rgb(255 255 255 / 0.08) inset;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,7 +30,11 @@ const colors = computed(() => ({
|
||||
text: isDarkMode.value ? '#9ca3af' : '#6b7280'
|
||||
}))
|
||||
|
||||
const hasData = computed(() => (props.data?.total ?? 0) > 0)
|
||||
const totalSlaErrors = computed(() =>
|
||||
(props.data?.items ?? []).reduce((total, item) => total + Number(item.sla || 0), 0)
|
||||
)
|
||||
|
||||
const hasData = computed(() => totalSlaErrors.value > 0)
|
||||
|
||||
const state = computed<ChartState>(() => {
|
||||
if (hasData.value) return 'ready'
|
||||
@@ -54,7 +58,7 @@ const categories = computed<ErrorCategory[]>(() => {
|
||||
|
||||
for (const item of props.data.items || []) {
|
||||
const code = Number(item.status_code || 0)
|
||||
const count = Number(item.total || 0)
|
||||
const count = Number(item.sla || 0)
|
||||
if (!Number.isFinite(code) || !Number.isFinite(count)) continue
|
||||
|
||||
if ([502, 503, 504].includes(code)) upstream += count
|
||||
|
||||
@@ -45,9 +45,7 @@ const colors = computed(() => ({
|
||||
text: isDarkMode.value ? '#9ca3af' : '#6b7280'
|
||||
}))
|
||||
|
||||
const totalRequestErrors = computed(() =>
|
||||
sumNumbers(props.points.map((p) => (p.error_count_sla ?? 0) + (p.business_limited_count ?? 0)))
|
||||
)
|
||||
const totalRequestErrors = computed(() => sumNumbers(props.points.map((p) => p.error_count_sla ?? 0)))
|
||||
|
||||
const totalUpstreamErrors = computed(() =>
|
||||
sumNumbers(
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent } from 'vue'
|
||||
import OpsErrorDistributionChart from '../OpsErrorDistributionChart.vue'
|
||||
import OpsErrorTrendChart from '../OpsErrorTrendChart.vue'
|
||||
|
||||
vi.mock('chart.js', () => ({
|
||||
Chart: { register: vi.fn() },
|
||||
ArcElement: {},
|
||||
CategoryScale: {},
|
||||
Filler: {},
|
||||
Legend: {},
|
||||
LineElement: {},
|
||||
LinearScale: {},
|
||||
PointElement: {},
|
||||
Title: {},
|
||||
Tooltip: {},
|
||||
}))
|
||||
|
||||
vi.mock('vue-chartjs', async () => {
|
||||
const { defineComponent } = await import('vue')
|
||||
|
||||
return {
|
||||
Doughnut: defineComponent({
|
||||
name: 'Doughnut',
|
||||
props: {
|
||||
data: { type: Object, required: true },
|
||||
options: { type: Object, default: () => ({}) },
|
||||
},
|
||||
template: '<div class="doughnut-stub" />',
|
||||
}),
|
||||
Line: defineComponent({
|
||||
name: 'LineChartStub',
|
||||
props: {
|
||||
data: { type: Object, required: true },
|
||||
options: { type: Object, default: () => ({}) },
|
||||
},
|
||||
template: '<div class="line-stub" />',
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../utils/opsFormatters', () => ({
|
||||
formatHistoryLabel: (date: string | undefined) => date ?? '',
|
||||
sumNumbers: (values: Array<number | null | undefined>) =>
|
||||
values.reduce<number>((total, value) => total + (typeof value === 'number' && Number.isFinite(value) ? value : 0), 0),
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('vue-i18n')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const HelpTooltipStub = defineComponent({
|
||||
name: 'HelpTooltip',
|
||||
props: {
|
||||
content: { type: String, default: '' },
|
||||
},
|
||||
template: '<span class="help-tooltip-stub" />',
|
||||
})
|
||||
|
||||
const EmptyStateStub = defineComponent({
|
||||
name: 'EmptyState',
|
||||
props: {
|
||||
title: { type: String, default: '' },
|
||||
description: { type: String, default: '' },
|
||||
},
|
||||
template: '<div class="empty-state-stub" />',
|
||||
})
|
||||
|
||||
const globalStubs = {
|
||||
stubs: {
|
||||
HelpTooltip: HelpTooltipStub,
|
||||
EmptyState: EmptyStateStub,
|
||||
},
|
||||
}
|
||||
|
||||
describe('Ops SLA-scoped error charts', () => {
|
||||
it('错误分布图按 SLA 错误数统计,不把业务限制错误算进请求错误分布', () => {
|
||||
const wrapper = mount(OpsErrorDistributionChart, {
|
||||
props: {
|
||||
loading: false,
|
||||
data: {
|
||||
total: 10,
|
||||
items: [
|
||||
{ status_code: 400, total: 7, sla: 2, business_limited: 5 },
|
||||
{ status_code: 503, total: 3, sla: 0, business_limited: 3 },
|
||||
],
|
||||
},
|
||||
},
|
||||
global: globalStubs,
|
||||
})
|
||||
|
||||
const doughnut = wrapper.findComponent({ name: 'Doughnut' })
|
||||
expect(doughnut.exists()).toBe(true)
|
||||
expect(doughnut.props('data')).toMatchObject({
|
||||
labels: ['admin.ops.client'],
|
||||
datasets: [{ data: [2] }],
|
||||
})
|
||||
})
|
||||
|
||||
it('错误分布图在只有业务限制错误时显示为空态', () => {
|
||||
const wrapper = mount(OpsErrorDistributionChart, {
|
||||
props: {
|
||||
loading: false,
|
||||
data: {
|
||||
total: 4,
|
||||
items: [{ status_code: 500, total: 4, sla: 0, business_limited: 4 }],
|
||||
},
|
||||
},
|
||||
global: globalStubs,
|
||||
})
|
||||
|
||||
expect(wrapper.findComponent({ name: 'Doughnut' }).exists()).toBe(false)
|
||||
expect(wrapper.find('.empty-state-stub').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('错误趋势图的请求错误详情按钮只按 SLA 错误启用', () => {
|
||||
const wrapper = mount(OpsErrorTrendChart, {
|
||||
props: {
|
||||
loading: false,
|
||||
timeRange: '1h',
|
||||
points: [
|
||||
{
|
||||
bucket_start: '2026-05-18T00:00:00Z',
|
||||
error_count_total: 5,
|
||||
business_limited_count: 5,
|
||||
error_count_sla: 0,
|
||||
upstream_error_count_excl_429_529: 0,
|
||||
upstream_429_count: 0,
|
||||
upstream_529_count: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
global: globalStubs,
|
||||
})
|
||||
|
||||
const requestErrorsButton = wrapper.findAll('button')[0]
|
||||
expect(requestErrorsButton.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user