Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions internal/api/usage_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ type usageSourceFilterOption struct {
DisplayName string `json:"displayName"`
}

type modelFilterOption struct {
Value string `json:"value"`
Label string `json:"label"`
}

type usageEventFilterOptionsResponse struct {
Models []string `json:"models"`
Sources []usageSourceFilterOption `json:"sources"`
Expand All @@ -39,6 +44,7 @@ type usageEventPayload struct {
Timestamp string `json:"timestamp"`
APIKey string `json:"api_key,omitempty"`
Model string `json:"model"`
ModelAlias *string `json:"model_alias,omitempty"`
ReasoningEffort string `json:"reasoning_effort,omitempty"`
ServiceTier string `json:"service_tier,omitempty"`
ExecutorType string `json:"executor_type,omitempty"`
Expand Down Expand Up @@ -75,12 +81,20 @@ func registerUsageEventsRoute(
cpaAPIKeyProvider service.CPAAPIKeyProvider,
) {
router.GET("/usage/events/filters/models", func(c *gin.Context) {
models, err := loadUsageEventModelFilterOptions(c, usageProvider)
models, labels, err := loadUsageEventModelFilterOptions(c, usageProvider)
if err != nil {
writeInternalError(c, "list usage event model filter options failed", err)
return
}
c.JSON(http.StatusOK, gin.H{"models": models})
options := make([]modelFilterOption, 0, len(models))
for _, model := range models {
label := model
if alias, ok := labels[model]; ok {
label = alias
}
options = append(options, modelFilterOption{Value: model, Label: label})
}
c.JSON(http.StatusOK, gin.H{"models": options})
})

router.GET("/usage/events/filters/sources", func(c *gin.Context) {
Expand Down Expand Up @@ -165,6 +179,7 @@ func buildUsageEventsPayload(rows []servicedto.UsageEventRecord, resolver usageI
ID: id,
Timestamp: timeutil.FormatStorageTime(row.Timestamp),
APIKey: usageEventAPIKeyLabel(row.APIGroupKey, apiKeyInfos),
ModelAlias: row.ModelAlias,
Model: row.Model,
ReasoningEffort: strings.TrimSpace(row.ReasoningEffort),
ServiceTier: strings.TrimSpace(row.ServiceTier),
Expand Down Expand Up @@ -231,15 +246,15 @@ func usageEventPublicSource(row servicedto.UsageEventRecord, identity resolvedUs
}
}

func loadUsageEventModelFilterOptions(c *gin.Context, usageProvider service.UsageProvider) ([]string, error) {
func loadUsageEventModelFilterOptions(c *gin.Context, usageProvider service.UsageProvider) ([]string, map[string]string, error) {
if usageProvider == nil {
return []string{}, nil
return []string{}, nil, nil
}
options, err := usageProvider.ListUsageEventFilterOptions(c.Request.Context(), servicedto.UsageFilter{})
if err != nil {
return nil, err
return nil, nil, err
}
return options.Models, nil
return options.Models, options.ModelLabels, nil
}

func loadUsageEventSourceFilterOptions(c *gin.Context, usageIdentityProvider service.UsageIdentityProvider) ([]usageSourceFilterOption, error) {
Expand Down
2 changes: 1 addition & 1 deletion internal/api/usage_events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ func TestUsageEventModelFilterOptionsReturnsStableModels(t *testing.T) {
t.Fatalf("expected model filters endpoint to ignore query filters, got %+v", provider.lastFilter)
}
body := resp.Body.String()
if body != `{"models":["claude-sonnet","gpt-5"]}` {
if body != `{"models":[{"value":"claude-sonnet","label":"claude-sonnet"},{"value":"gpt-5","label":"gpt-5"}]}` {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In DTOs intended for frontend consumption, maintain unique identifiers in the 'key' field and use a separate 'label' field strictly for display purposes. Overwriting unique keys with display labels can cause rendering issues in frameworks like React and prevent the client from uniquely identifying data sources.

Suggested change
if body != `{"models":[{"value":"claude-sonnet","label":"claude-sonnet"},{"value":"gpt-5","label":"gpt-5"}]}` {
if body != "{\"models\":[{\"key\":\"claude-sonnet\",\"label\":\"claude-sonnet\"},{\"key\":\"gpt-5\",\"label\":\"gpt-5\"}]}" {
References
  1. In DTOs intended for frontend consumption, maintain unique identifiers in the 'key' field and use a separate 'label' field strictly for display purposes. Overwriting unique keys with display labels can cause rendering issues in frameworks like React and prevent the client from uniquely identifying data sources.

t.Fatalf("expected stable model filter options, got %s", body)
}
}
Expand Down
4 changes: 3 additions & 1 deletion internal/repository/dto/usage_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ type UsageEventsPageRecord struct {

// UsageEventFilterOptionsRecord 是 usage events 筛选项的仓储查询结果。
type UsageEventFilterOptionsRecord struct {
Models []string
Models []string
ModelLabels map[string]string
}

// UsageEventRecord 是单条 usage event 的查询结果。
Expand All @@ -23,6 +24,7 @@ type UsageEventRecord struct {
Timestamp time.Time
APIGroupKey string
Model string
ModelAlias *string
ReasoningEffort string
ServiceTier string
ExecutorType string
Expand Down
141 changes: 124 additions & 17 deletions internal/repository/usage.go

Large diffs are not rendered by default.

29 changes: 15 additions & 14 deletions internal/repository/usage_window_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ type UsageWindowStatsCalculator struct {
}

type usageWindowTokenStats struct {
Model string `gorm:"column:model"`
TotalTokens int64 `gorm:"column:total_tokens"`
InputTokens int64 `gorm:"column:input_tokens"`
OutputTokens int64 `gorm:"column:output_tokens"`
CachedTokens int64 `gorm:"column:cached_tokens"`
CacheReadTokens int64 `gorm:"column:cache_read_tokens"`
CacheCreationTokens int64 `gorm:"column:cache_creation_tokens"`
Model string `gorm:"column:model"`
ModelAlias *string `gorm:"column:model_alias"`
TotalTokens int64 `gorm:"column:total_tokens"`
InputTokens int64 `gorm:"column:input_tokens"`
OutputTokens int64 `gorm:"column:output_tokens"`
CachedTokens int64 `gorm:"column:cached_tokens"`
CacheReadTokens int64 `gorm:"column:cache_read_tokens"`
CacheCreationTokens int64 `gorm:"column:cache_creation_tokens"`
}

func NewUsageWindowStatsCalculator(ctx context.Context, db *gorm.DB) (*UsageWindowStatsCalculator, error) {
Expand Down Expand Up @@ -208,11 +209,11 @@ func sumRawUsageWindowTokenStats(db *gorm.DB, authIndex string, start time.Time,
// raw 查询只取 model 级汇总字段,避免把大量 usage_events 行读进 Go 内存。
query := db.Model(&entities.UsageEvent{}).
// SELECT 中只聚合 token/cost 需要的字段,不读取 raw_json 等大字段。
Select("model, COALESCE(SUM(total_tokens), 0) AS total_tokens, COALESCE(SUM(input_tokens), 0) AS input_tokens, COALESCE(SUM(output_tokens), 0) AS output_tokens, COALESCE(SUM(cached_tokens), 0) AS cached_tokens, COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens, COALESCE(SUM(cache_creation_tokens), 0) AS cache_creation_tokens").
Select("model, model_alias, COALESCE(SUM(total_tokens), 0) AS total_tokens, COALESCE(SUM(input_tokens), 0) AS input_tokens, COALESCE(SUM(output_tokens), 0) AS output_tokens, COALESCE(SUM(cached_tokens), 0) AS cached_tokens, COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens, COALESCE(SUM(cache_creation_tokens), 0) AS cache_creation_tokens").
// auth_index 已经是唯一身份维度,这里不再额外按 auth_type 过滤。
Where("auth_index = ? AND timestamp >= ?", authIndex, timeutil.FormatStorageTime(start)).
// 按 model 分组,后续按 model 价格表计算 cost。
Group("model")
// 按 model + model_alias 分组,后续按 model/alias 价格表计算 cost。
Group("model, model_alias")
// 如果调用方传入结束时间,就用半开区间避免边界重复累计。
if end != nil {
// end 统一格式化为 storage time,确保 SQLite 文本比较稳定。
Expand All @@ -233,11 +234,11 @@ func sumHourlyUsageWindowTokenStats(db *gorm.DB, authIndex string, start time.Ti
// hourly 查询直接读取 overview 已经维护好的小时增量表。
query := db.Model(&entities.UsageOverviewHourlyStat{}).
// SELECT 中聚合 token/cost 需要的字段,保持和 raw 查询返回结构一致。
Select("model, COALESCE(SUM(total_tokens), 0) AS total_tokens, COALESCE(SUM(input_tokens), 0) AS input_tokens, COALESCE(SUM(output_tokens), 0) AS output_tokens, COALESCE(SUM(cached_tokens), 0) AS cached_tokens, COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens, COALESCE(SUM(cache_creation_tokens), 0) AS cache_creation_tokens").
Select("model, model_alias, COALESCE(SUM(total_tokens), 0) AS total_tokens, COALESCE(SUM(input_tokens), 0) AS input_tokens, COALESCE(SUM(output_tokens), 0) AS output_tokens, COALESCE(SUM(cached_tokens), 0) AS cached_tokens, COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens, COALESCE(SUM(cache_creation_tokens), 0) AS cache_creation_tokens").
// auth_index + bucket_start 范围可以使用现有 hourly auth_bucket 索引。
Where("auth_index = ? AND bucket_start >= ? AND bucket_start < ?", authIndex, timeutil.FormatStorageTime(start), timeutil.FormatStorageTime(end)).
// 按 model 分组,后续按 model 价格表计算 cost。
Group("model")
// 按 model + model_alias 分组,后续按 model/alias 价格表计算 cost。
Group("model, model_alias")
// rows 只承接聚合后的少量 model 行。
var rows []usageWindowTokenStats
// 执行 hourly 聚合查询。
Expand Down Expand Up @@ -295,7 +296,7 @@ func usageWindowStatsFromTokenStats(rows []usageWindowTokenStats, pricingByModel
// total_tokens 直接累计到前端展示的窗口 token。
stats.Tokens += row.TotalTokens
// model 名称按 trim 后查价格,保持和其它 Overview/Usage cost 逻辑一致。
pricing := pricingByModel[strings.TrimSpace(row.Model)]
pricing, _ := lookupPricingByModelOrAlias(pricingByModel, row.Model, row.ModelAlias)
// 使用统一 helper 按当前价格表计算该 model 的 cost。
stats.Cost += helper.CalculateUsageTokenCost(helper.UsageTokenCostInput{
InputTokens: row.InputTokens,
Expand Down
4 changes: 3 additions & 1 deletion internal/service/dto/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ type UsageEventsPage struct {

// UsageEventFilterOptions 是 usage events 筛选项的服务层结果。
type UsageEventFilterOptions struct {
Models []string
Models []string
ModelLabels map[string]string
}

// UsageEventRecord 是单条 usage event 的服务层结果。
Expand All @@ -50,6 +51,7 @@ type UsageEventRecord struct {
Timestamp time.Time
APIGroupKey string
Model string
ModelAlias *string
ReasoningEffort string
ServiceTier string
ExecutorType string
Expand Down
3 changes: 2 additions & 1 deletion internal/service/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ func (s *usageService) ListUsageEvents(_ context.Context, filter servicedto.Usag
Timestamp: row.Timestamp,
APIGroupKey: row.APIGroupKey,
Model: row.Model,
ModelAlias: row.ModelAlias,
ReasoningEffort: row.ReasoningEffort,
ServiceTier: row.ServiceTier,
ExecutorType: row.ExecutorType,
Expand Down Expand Up @@ -471,5 +472,5 @@ func (s *usageService) ListUsageEventFilterOptions(_ context.Context, filter ser
if err != nil {
return nil, err
}
return &servicedto.UsageEventFilterOptions{Models: options.Models}, nil
return &servicedto.UsageEventFilterOptions{Models: options.Models, ModelLabels: options.ModelLabels}, nil
}
2 changes: 1 addition & 1 deletion web/src/components/usage/RequestEventsDetailsCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const renderCard = (props: Partial<React.ComponentProps<typeof RequestEventsDeta
pageSizeOptions={[20, 50, 100, 500, 1000]}
totalCount={120}
totalPages={6}
modelOptions={['claude-sonnet', 'claude-opus']}
modelOptions={[{ value: 'claude-sonnet', label: 'claude-sonnet' }, { value: 'claude-opus', label: 'claude-opus' }]}
sourceOptions={[{ value: 'source-a', label: 'Provider A' }, { value: 'source-b', label: 'Provider B' }]}
modelFilter="__all__"
sourceFilter="__all__"
Expand Down
9 changes: 5 additions & 4 deletions web/src/components/usage/RequestEventsDetailsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Card } from '@/components/ui/Card';
import { EmptyState } from '@/components/ui/EmptyState';
import { Select } from '@/components/ui/Select';
import { IconCheck, IconChevronDown } from '@/components/ui/icons';
import type { UsageEvent, UsageSourceFilterOption } from '@/lib/types';
import type { ModelFilterOption, UsageEvent, UsageSourceFilterOption } from '@/lib/types';
import {
calculateCacheRate,
formatDurationMs,
Expand Down Expand Up @@ -148,7 +148,7 @@ export interface RequestEventsDetailsCardProps {
pageSizeOptions: readonly number[];
totalCount: number;
totalPages: number;
modelOptions: string[];
modelOptions: ModelFilterOption[];
sourceOptions: UsageSourceFilterOption[];
modelFilter: string;
sourceFilter: string;
Expand Down Expand Up @@ -531,7 +531,8 @@ export function RequestEventsDetailsCard({
const source = String(event.source ?? '').trim() || '-';
const sourceType = String(event.source_type ?? '').trim();
const apiKey = String(event.api_key ?? '').trim() || '-';
const model = String(event.model ?? '').trim() || '-';
const modelAlias = String(event.model_alias ?? '').trim();
const model = modelAlias || String(event.model ?? '').trim() || '-';
const reasoningEffort = String(event.reasoning_effort ?? '').trim() || '-';
const serviceTier = formatRequestSpeedMode(event.service_tier, t);
const endpointFields = parseRequestEndpoint(event.endpoint);
Expand Down Expand Up @@ -606,7 +607,7 @@ export function RequestEventsDetailsCard({
const modelOptions = useMemo(() => {
const options = [
{ value: ALL_FILTER, label: t('usage_stats.filter_all') },
...backendModelOptions.map((model) => ({ value: model, label: model })),
...backendModelOptions,
];
return appendSelectedOption(options, modelFilter);
}, [backendModelOptions, modelFilter, t]);
Expand Down
16 changes: 8 additions & 8 deletions web/src/components/usage/analysis/AnalysisPanel.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -748,8 +748,8 @@
border-radius: 5px;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.20),
inset 0 -10px 18px rgba(127, 29, 29, 0.18),
0 1px 3px rgba(67, 20, 7, 0.10);
inset 0 -10px 18px rgba(80, 70, 60, 0.12),
0 1px 3px rgba(60, 50, 40, 0.08);
font-variant-numeric: tabular-nums;
outline: none;
}
Expand All @@ -759,16 +759,16 @@
position: absolute;
inset: 0;
background:
radial-gradient(circle at 50% 115%, rgba(255, 247, 237, 0.82), rgba(249, 115, 22, 0.36) 34%, rgba(127, 29, 29, 0) 72%),
linear-gradient(180deg, rgba(255, 255, 255, 0.24), rgba(255, 255, 255, 0) 38%);
radial-gradient(circle at 50% 115%, rgba(245, 240, 235, 0.60), rgba(180, 170, 160, 0.20) 34%, rgba(120, 110, 100, 0) 72%),
linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0) 38%);
opacity: var(--heatmap-flame-alpha, 0.35);
pointer-events: none;
}

.heatmapCardDark .heatmapCell::before {
background:
radial-gradient(circle at 50% 115%, rgba(254, 243, 199, 0.92), rgba(249, 115, 22, 0.48) 36%, rgba(127, 29, 29, 0) 74%),
linear-gradient(180deg, rgba(255, 247, 237, 0.10), rgba(255, 247, 237, 0) 42%);
radial-gradient(circle at 50% 115%, rgba(220, 215, 210, 0.50), rgba(160, 152, 144, 0.22) 36%, rgba(100, 90, 80, 0) 74%),
linear-gradient(180deg, rgba(240, 238, 232, 0.08), rgba(240, 238, 232, 0) 42%);
}

.heatmapCell:focus-visible {
Expand Down Expand Up @@ -803,12 +803,12 @@
width: 120px;
height: 10px;
border-radius: 8px;
background: linear-gradient(90deg, #fff7ed, #fed7aa, #fb923c, #ef4444, #7c2d12);
background: linear-gradient(90deg, #f0eee8, #cec8c0, #ada59c, #8b8378, #5c524a);
box-shadow: inset 0 0 0 1px var(--border-color);
}

.heatmapCardDark .heatmapLegendRamp {
background: linear-gradient(90deg, #1a1118, #4a1f23, #9a3412, #f97316, #fde68a);
background: linear-gradient(90deg, #1a1815, #37322e, #5a534c, #82786e, #b2aaa3);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.10);
}

Expand Down
24 changes: 12 additions & 12 deletions web/src/components/usage/analysis/AnalysisPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -471,18 +471,18 @@ const getHeatmapCellColor = (intensity: number, isDark: boolean) => {
const stops: Array<{ at: number; color: [number, number, number] }> = [
...(isDark
? [
{ at: 0, color: [26, 17, 24] },
{ at: 0.24, color: [74, 31, 35] },
{ at: 0.48, color: [154, 52, 18] },
{ at: 0.74, color: [249, 115, 22] },
{ at: 1, color: [253, 230, 138] },
{ at: 0, color: [26, 24, 23] },
{ at: 0.25, color: [55, 50, 47] },
{ at: 0.5, color: [90, 83, 76] },
{ at: 0.75, color: [130, 120, 112] },
{ at: 1, color: [178, 170, 163] },
] satisfies Array<{ at: number; color: [number, number, number] }>
: [
{ at: 0, color: [255, 247, 237] },
{ at: 0.22, color: [254, 215, 170] },
{ at: 0.48, color: [251, 146, 60] },
{ at: 0.72, color: [239, 68, 68] },
{ at: 1, color: [124, 45, 18] },
{ at: 0, color: [240, 238, 232] },
{ at: 0.2, color: [206, 200, 192] },
{ at: 0.45, color: [173, 165, 156] },
{ at: 0.7, color: [139, 131, 124] },
{ at: 1, color: [92, 82, 74] },
] satisfies Array<{ at: number; color: [number, number, number] }>),
];
const upperIndex = stops.findIndex((stop) => clampedIntensity <= stop.at);
Expand All @@ -496,9 +496,9 @@ const getHeatmapCellColor = (intensity: number, isDark: boolean) => {
const getHeatmapCellTextColor = (intensity: number, isDark: boolean) => {
const clampedIntensity = Math.max(0, Math.min(1, intensity));
if (!isDark) {
return clampedIntensity > 0.58 ? '#fff7ed' : '#431407';
return clampedIntensity > 0.58 ? '#f5f3ef' : '#3a3530';
}
return clampedIntensity > 0.86 ? '#1c1208' : '#fff7ed';
return clampedIntensity > 0.75 ? '#1a1817' : '#f0eee8';
};

const getHeatmapVisualIntensity = (value: number, maxValue: number) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function AiProviderCredentialsSection({ rows, total, page, totalPages, pa
{row.priorityLabel && <CredentialPriorityBadge>{row.priorityLabel}</CredentialPriorityBadge>}
</span>
)}
badges={null}
badges={<CredentialBadge tone="neutral">{row.maskedIdentity}</CredentialBadge>}
metrics={(
<>
<MetricPill value={<RequestMetric total={row.totalRequests} success={row.successCount} failure={row.failureCount} />} />
Expand Down
8 changes: 7 additions & 1 deletion web/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export interface UsageEvent {
timestamp: string
api_key?: string
model: string
model_alias?: string
reasoning_effort?: string
service_tier?: string
executor_type?: string
Expand Down Expand Up @@ -247,8 +248,13 @@ export interface UsageEventsResponse {
total_pages: number
}

export interface ModelFilterOption {
value: string
label: string
}

export interface UsageEventModelFilterOptionsResponse {
models: string[]
models: ModelFilterOption[]
}

export interface UsageEventSourceFilterOptionsResponse {
Expand Down
4 changes: 2 additions & 2 deletions web/src/pages/UsagePage.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ describe('UsagePage request event filters', () => {
result: 'failed',
},
{
models: ['claude-sonnet'],
models: [{ value: 'claude-sonnet', label: 'claude-sonnet' }],
sources: [{ value: 'authidx-source-a', label: 'authidx-source-a' }],
},
);
Expand All @@ -517,7 +517,7 @@ describe('UsagePage request event filters', () => {
result: 'success',
},
{
models: ['claude-sonnet'],
models: [{ value: 'claude-sonnet', label: 'claude-sonnet' }],
sources: [{ value: 'authidx-source-a', label: 'authidx-source-a' }],
},
);
Expand Down
4 changes: 2 additions & 2 deletions web/src/pages/UsagePage.styles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,8 @@ describe('UsagePage toolbar styles', () => {
expect(heatmapRowLabelBlock).toContain('align-self: center;')
expect(analysisPanelStyles).toMatch(/\.heatmapModelLabel\s*\{[\s\S]*?-webkit-line-clamp:\s*2;/)
expect(analysisPanelStyles).toMatch(/\.heatmapModelLabel\s*\{[\s\S]*?overflow-wrap:\s*anywhere;/)
expect(analysisPanelStyles).toMatch(/\.heatmapLegendRamp\s*\{[\s\S]*?linear-gradient\(90deg, #fff7ed, #fed7aa, #fb923c, #ef4444, #7c2d12\)/)
expect(analysisPanelStyles).toMatch(/\.heatmapCardDark \.heatmapLegendRamp\s*\{[\s\S]*?linear-gradient\(90deg, #1a1118, #4a1f23, #9a3412, #f97316, #fde68a\)/)
expect(analysisPanelStyles).toMatch(/\.heatmapLegendRamp\s*\{[\s\S]*?linear-gradient\(90deg, #f0eee8, #cec8c0, #ada59c, #8b8378, #5c524a\)/)
expect(analysisPanelStyles).toMatch(/\.heatmapCardDark \.heatmapLegendRamp\s*\{[\s\S]*?linear-gradient\(90deg, #1a1815, #37322e, #5a534c, #82786e, #b2aaa3\)/)
expect(analysisPanelStyles).toMatch(/\.heatmapFloatingTooltip\s*\{[\s\S]*?position:\s*fixed;/)
expect(analysisPanelStyles).toMatch(/\.heatmapFloatingTooltip\s*\{[\s\S]*?border:\s*1px solid var\(--border-color\);/)
expect(analysisPanelStyles).toMatch(/\.heatmapFloatingTooltip\s*\{[\s\S]*?background:\s*var\(--bg-primary\);/)
Expand Down
Loading