feat(ui): improve table controls and analytics filters
This commit is contained in:
+23
-15
@@ -5,43 +5,51 @@ import { useTranslation } from 'react-i18next'
|
||||
import type { TimeGranularity } from '@/lib/time'
|
||||
import { VCHART_OPTION } from '@/lib/vchart'
|
||||
import { useTheme } from '@/context/theme-provider'
|
||||
import { DEFAULT_TIME_GRANULARITY } from '@/features/dashboard/constants'
|
||||
import {
|
||||
CONSUMPTION_DISTRIBUTION_CHART_OPTIONS,
|
||||
DEFAULT_TIME_GRANULARITY,
|
||||
} from '@/features/dashboard/constants'
|
||||
import { processChartData } from '@/features/dashboard/lib'
|
||||
import type { QuotaDataItem } from '@/features/dashboard/types'
|
||||
import type {
|
||||
ConsumptionDistributionChartType,
|
||||
QuotaDataItem,
|
||||
} from '@/features/dashboard/types'
|
||||
|
||||
let themeManagerPromise: Promise<
|
||||
(typeof import('@visactor/vchart'))['ThemeManager']
|
||||
> | null = null
|
||||
|
||||
type DistributionChartType = 'bar' | 'area'
|
||||
|
||||
interface ConsumptionDistributionChartProps {
|
||||
data: QuotaDataItem[]
|
||||
loading?: boolean
|
||||
timeGranularity?: TimeGranularity
|
||||
defaultChartType?: ConsumptionDistributionChartType
|
||||
}
|
||||
|
||||
const CHART_TYPES: Array<{
|
||||
value: DistributionChartType
|
||||
labelKey: string
|
||||
icon: typeof BarChart3
|
||||
}> = [
|
||||
{ value: 'bar', labelKey: 'Bar Chart', icon: BarChart3 },
|
||||
{ value: 'area', labelKey: 'Area Chart', icon: AreaChart },
|
||||
]
|
||||
const CHART_TYPE_ICONS: Record<ConsumptionDistributionChartType, typeof BarChart3> =
|
||||
{
|
||||
bar: BarChart3,
|
||||
area: AreaChart,
|
||||
}
|
||||
|
||||
export function ConsumptionDistributionChart(
|
||||
props: ConsumptionDistributionChartProps
|
||||
) {
|
||||
const { t } = useTranslation()
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [chartType, setChartType] = useState<DistributionChartType>('bar')
|
||||
const [chartType, setChartType] = useState<ConsumptionDistributionChartType>(
|
||||
props.defaultChartType ?? 'bar'
|
||||
)
|
||||
const [themeReady, setThemeReady] = useState(false)
|
||||
const themeManagerRef = useRef<
|
||||
(typeof import('@visactor/vchart'))['ThemeManager'] | null
|
||||
>(null)
|
||||
const timeGranularity = props.timeGranularity ?? DEFAULT_TIME_GRANULARITY
|
||||
|
||||
useEffect(() => {
|
||||
if (props.defaultChartType) setChartType(props.defaultChartType)
|
||||
}, [props.defaultChartType])
|
||||
|
||||
useEffect(() => {
|
||||
const updateTheme = async () => {
|
||||
setThemeReady(false)
|
||||
@@ -81,8 +89,8 @@ export function ConsumptionDistributionChart(
|
||||
</div>
|
||||
|
||||
<div className='bg-muted/60 inline-flex h-8 rounded-md border p-0.5'>
|
||||
{CHART_TYPES.map((item) => {
|
||||
const Icon = item.icon
|
||||
{CONSUMPTION_DISTRIBUTION_CHART_OPTIONS.map((item) => {
|
||||
const Icon = CHART_TYPE_ICONS[item.value]
|
||||
return (
|
||||
<button
|
||||
key={item.value}
|
||||
|
||||
@@ -5,47 +5,51 @@ import { useTranslation } from 'react-i18next'
|
||||
import type { TimeGranularity } from '@/lib/time'
|
||||
import { VCHART_OPTION } from '@/lib/vchart'
|
||||
import { useTheme } from '@/context/theme-provider'
|
||||
import { DEFAULT_TIME_GRANULARITY } from '@/features/dashboard/constants'
|
||||
import {
|
||||
DEFAULT_TIME_GRANULARITY,
|
||||
MODEL_ANALYTICS_CHART_OPTIONS,
|
||||
} from '@/features/dashboard/constants'
|
||||
import { processChartData } from '@/features/dashboard/lib'
|
||||
import type { QuotaDataItem } from '@/features/dashboard/types'
|
||||
import type {
|
||||
ModelAnalyticsChartTab,
|
||||
QuotaDataItem,
|
||||
} from '@/features/dashboard/types'
|
||||
|
||||
let themeManagerPromise: Promise<
|
||||
(typeof import('@visactor/vchart'))['ThemeManager']
|
||||
> | null = null
|
||||
|
||||
type ChartTab = 'trend' | 'proportion' | 'top'
|
||||
type ChartSpecKey = 'spec_model_line' | 'spec_pie' | 'spec_rank_bar'
|
||||
|
||||
const CHART_TABS: {
|
||||
value: ChartTab
|
||||
labelKey: string
|
||||
specKey: ChartSpecKey
|
||||
}[] = [
|
||||
{ value: 'trend', labelKey: 'Call Trend', specKey: 'spec_model_line' },
|
||||
{
|
||||
value: 'proportion',
|
||||
labelKey: 'Call Count Distribution',
|
||||
specKey: 'spec_pie',
|
||||
},
|
||||
{ value: 'top', labelKey: 'Call Count Ranking', specKey: 'spec_rank_bar' },
|
||||
]
|
||||
const CHART_SPEC_KEYS: Record<ModelAnalyticsChartTab, ChartSpecKey> = {
|
||||
trend: 'spec_model_line',
|
||||
proportion: 'spec_pie',
|
||||
top: 'spec_rank_bar',
|
||||
}
|
||||
|
||||
interface ModelChartsProps {
|
||||
data: QuotaDataItem[]
|
||||
loading?: boolean
|
||||
timeGranularity?: TimeGranularity
|
||||
defaultChartTab?: ModelAnalyticsChartTab
|
||||
}
|
||||
|
||||
export function ModelCharts(props: ModelChartsProps) {
|
||||
const { t } = useTranslation()
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [activeTab, setActiveTab] = useState<ChartTab>('trend')
|
||||
const [activeTab, setActiveTab] = useState<ModelAnalyticsChartTab>(
|
||||
props.defaultChartTab ?? 'trend'
|
||||
)
|
||||
const [themeReady, setThemeReady] = useState(false)
|
||||
const themeManagerRef = useRef<
|
||||
(typeof import('@visactor/vchart'))['ThemeManager'] | null
|
||||
>(null)
|
||||
const timeGranularity = props.timeGranularity ?? DEFAULT_TIME_GRANULARITY
|
||||
|
||||
useEffect(() => {
|
||||
if (props.defaultChartTab) setActiveTab(props.defaultChartTab)
|
||||
}, [props.defaultChartTab])
|
||||
|
||||
useEffect(() => {
|
||||
const updateTheme = async () => {
|
||||
setThemeReady(false)
|
||||
@@ -70,8 +74,7 @@ export function ModelCharts(props: ModelChartsProps) {
|
||||
[props.data, props.loading, timeGranularity, t]
|
||||
)
|
||||
|
||||
const activeSpec = CHART_TABS.find((tab) => tab.value === activeTab)
|
||||
const spec = activeSpec ? chartData[activeSpec.specKey] : null
|
||||
const spec = chartData[CHART_SPEC_KEYS[activeTab]]
|
||||
|
||||
return (
|
||||
<div className='overflow-hidden rounded-lg border'>
|
||||
@@ -87,7 +90,7 @@ export function ModelCharts(props: ModelChartsProps) {
|
||||
</div>
|
||||
|
||||
<div className='bg-muted/60 inline-flex h-8 rounded-md border p-0.5'>
|
||||
{CHART_TABS.map((tab) => (
|
||||
{MODEL_ANALYTICS_CHART_OPTIONS.map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
type='button'
|
||||
|
||||
+21
-28
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Filter, RotateCcw, Calendar, Search } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
@@ -26,20 +26,20 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
import { DateTimePicker } from '@/components/datetime-picker'
|
||||
import {
|
||||
DEFAULT_TIME_GRANULARITY,
|
||||
TIME_GRANULARITY_OPTIONS,
|
||||
TIME_RANGE_PRESETS,
|
||||
EMPTY_DASHBOARD_FILTERS,
|
||||
} from '@/features/dashboard/constants'
|
||||
import {
|
||||
buildDefaultDashboardFilters,
|
||||
cleanFilters,
|
||||
getSavedGranularity,
|
||||
saveGranularity,
|
||||
getDefaultDays,
|
||||
} from '@/features/dashboard/lib'
|
||||
import { type DashboardFilters } from '@/features/dashboard/types'
|
||||
import type {
|
||||
DashboardChartPreferences,
|
||||
DashboardFilters,
|
||||
} from '@/features/dashboard/types'
|
||||
|
||||
interface ModelsFilterProps {
|
||||
preferences: DashboardChartPreferences
|
||||
onFilterChange: (filters: DashboardFilters) => void
|
||||
onReset: () => void
|
||||
}
|
||||
@@ -58,30 +58,27 @@ const SectionDivider = ({ label }: { label: string }) => (
|
||||
</div>
|
||||
)
|
||||
|
||||
export function ModelsFilter({ onFilterChange, onReset }: ModelsFilterProps) {
|
||||
export function ModelsFilter(props: ModelsFilterProps) {
|
||||
const { t } = useTranslation()
|
||||
// 使用已缓存的用户数据,避免重复调用 API
|
||||
const user = useAuthStore((state) => state.auth.user)
|
||||
const isAdmin = user?.role && user.role >= 10
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [filters, setFilters] = useState<DashboardFilters>(() => {
|
||||
const granularity = getSavedGranularity()
|
||||
const days = getDefaultDays(granularity)
|
||||
const { start, end } = getNormalizedDateRange(days)
|
||||
return {
|
||||
...EMPTY_DASHBOARD_FILTERS,
|
||||
start_timestamp: start,
|
||||
end_timestamp: end,
|
||||
time_granularity: granularity,
|
||||
}
|
||||
})
|
||||
const [filters, setFilters] = useState<DashboardFilters>(() =>
|
||||
buildDefaultDashboardFilters(props.preferences)
|
||||
)
|
||||
const [selectedRange, setSelectedRange] = useState<number | null>(() =>
|
||||
getDefaultDays()
|
||||
props.preferences.defaultTimeRangeDays
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setFilters(buildDefaultDashboardFilters(props.preferences))
|
||||
setSelectedRange(props.preferences.defaultTimeRangeDays)
|
||||
}, [props.preferences])
|
||||
|
||||
const handleApply = () => {
|
||||
onFilterChange(
|
||||
props.onFilterChange(
|
||||
cleanFilters(
|
||||
filters as unknown as Record<string, unknown>
|
||||
) as typeof filters
|
||||
@@ -90,17 +87,15 @@ export function ModelsFilter({ onFilterChange, onReset }: ModelsFilterProps) {
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
const days = getDefaultDays(DEFAULT_TIME_GRANULARITY)
|
||||
const days = props.preferences.defaultTimeRangeDays
|
||||
const { start, end } = getNormalizedDateRange(days)
|
||||
setFilters({
|
||||
...EMPTY_DASHBOARD_FILTERS,
|
||||
...buildDefaultDashboardFilters(props.preferences),
|
||||
start_timestamp: start,
|
||||
end_timestamp: end,
|
||||
time_granularity: DEFAULT_TIME_GRANULARITY,
|
||||
})
|
||||
setSelectedRange(days)
|
||||
saveGranularity(DEFAULT_TIME_GRANULARITY)
|
||||
onReset()
|
||||
props.onReset()
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
@@ -111,8 +106,6 @@ export function ModelsFilter({ onFilterChange, onReset }: ModelsFilterProps) {
|
||||
setFilters((prev) => ({ ...prev, [field]: value }))
|
||||
if (field === 'start_timestamp' || field === 'end_timestamp')
|
||||
setSelectedRange(null)
|
||||
if (field === 'time_granularity' && typeof value === 'string')
|
||||
saveGranularity(value as TimeGranularity)
|
||||
}
|
||||
|
||||
const handleQuickRange = (days: number) => {
|
||||
|
||||
+21
-1
@@ -1,9 +1,18 @@
|
||||
import type { DashboardFilters } from './types'
|
||||
import type { DashboardChartPreferences, DashboardFilters } from './types'
|
||||
|
||||
export const TIME_GRANULARITY_STORAGE_KEY = 'data_export_default_time'
|
||||
export const DASHBOARD_CHART_PREFERENCES_STORAGE_KEY =
|
||||
'dashboard_models_chart_preferences'
|
||||
export const DEFAULT_TIME_GRANULARITY = 'hour' as const
|
||||
export const MAX_CHART_TREND_POINTS = 7
|
||||
|
||||
export const DEFAULT_DASHBOARD_CHART_PREFERENCES: DashboardChartPreferences = {
|
||||
consumptionDistributionChart: 'bar',
|
||||
modelAnalyticsChart: 'trend',
|
||||
defaultTimeRangeDays: 1,
|
||||
defaultTimeGranularity: DEFAULT_TIME_GRANULARITY,
|
||||
}
|
||||
|
||||
export const TIME_RANGE_BY_GRANULARITY = {
|
||||
hour: 1,
|
||||
day: 7,
|
||||
@@ -23,6 +32,17 @@ export const TIME_RANGE_PRESETS = [
|
||||
{ label: '29 Days', days: 29 },
|
||||
] as const
|
||||
|
||||
export const CONSUMPTION_DISTRIBUTION_CHART_OPTIONS = [
|
||||
{ value: 'bar', labelKey: 'Bar Chart' },
|
||||
{ value: 'area', labelKey: 'Area Chart' },
|
||||
] as const
|
||||
|
||||
export const MODEL_ANALYTICS_CHART_OPTIONS = [
|
||||
{ value: 'trend', labelKey: 'Call Trend' },
|
||||
{ value: 'proportion', labelKey: 'Call Count Distribution' },
|
||||
{ value: 'top', labelKey: 'Call Count Ranking' },
|
||||
] as const
|
||||
|
||||
export const EMPTY_DASHBOARD_FILTERS: DashboardFilters = {
|
||||
start_timestamp: undefined,
|
||||
end_timestamp: undefined,
|
||||
|
||||
+66
-22
@@ -11,6 +11,12 @@ import {
|
||||
CardStaggerItem,
|
||||
FadeIn,
|
||||
} from '@/components/page-transition'
|
||||
import {
|
||||
buildDefaultDashboardFilters,
|
||||
getSavedChartPreferences,
|
||||
saveChartPreferences,
|
||||
} from './lib'
|
||||
import { ModelsChartPreferences } from './components/models/models-chart-preferences'
|
||||
import { ModelsFilter } from './components/models/models-filter-dialog'
|
||||
import { AnnouncementsPanel } from './components/overview/announcements-panel'
|
||||
import { ApiInfoPanel } from './components/overview/api-info-panel'
|
||||
@@ -23,7 +29,11 @@ import {
|
||||
DASHBOARD_DEFAULT_SECTION,
|
||||
DASHBOARD_SECTION_IDS,
|
||||
} from './section-registry'
|
||||
import { type DashboardFilters, type QuotaDataItem } from './types'
|
||||
import {
|
||||
type DashboardChartPreferences,
|
||||
type DashboardFilters,
|
||||
type QuotaDataItem,
|
||||
} from './types'
|
||||
|
||||
const route = getRouteApi('/_authenticated/dashboard/$section')
|
||||
|
||||
@@ -107,17 +117,21 @@ export function Dashboard() {
|
||||
const activeSection = (params.section ??
|
||||
DASHBOARD_DEFAULT_SECTION) as DashboardSectionId
|
||||
|
||||
const [modelFilters, setModelFilters] = useState<DashboardFilters>({})
|
||||
const [modelData, setModelData] = useState<QuotaDataItem[]>([])
|
||||
const [dataLoading, setDataLoading] = useState(false)
|
||||
const [chartPreferences, setChartPreferences] =
|
||||
useState<DashboardChartPreferences>(() => getSavedChartPreferences())
|
||||
const [modelFilters, setModelFilters] = useState<DashboardFilters>(() =>
|
||||
buildDefaultDashboardFilters(getSavedChartPreferences())
|
||||
)
|
||||
|
||||
const handleFilterChange = useCallback((filters: DashboardFilters) => {
|
||||
setModelFilters(filters)
|
||||
}, [])
|
||||
|
||||
const handleResetFilters = useCallback(() => {
|
||||
setModelFilters({})
|
||||
}, [])
|
||||
setModelFilters(buildDefaultDashboardFilters(chartPreferences))
|
||||
}, [chartPreferences])
|
||||
|
||||
const handleDataUpdate = useCallback(
|
||||
(data: QuotaDataItem[], loading: boolean) => {
|
||||
@@ -127,6 +141,15 @@ export function Dashboard() {
|
||||
[]
|
||||
)
|
||||
|
||||
const handleChartPreferencesChange = useCallback(
|
||||
(preferences: DashboardChartPreferences) => {
|
||||
setChartPreferences(preferences)
|
||||
setModelFilters(buildDefaultDashboardFilters(preferences))
|
||||
saveChartPreferences(preferences)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const meta = SECTION_META[activeSection] ?? SECTION_META.overview
|
||||
const isAdmin = Boolean(userRole && userRole >= ROLE.ADMIN)
|
||||
const visibleSections = useMemo(
|
||||
@@ -146,6 +169,20 @@ export function Dashboard() {
|
||||
[navigate]
|
||||
)
|
||||
const showSectionTabs = activeSection !== 'overview' && visibleSections.length > 1
|
||||
const modelActions =
|
||||
activeSection === 'models' ? (
|
||||
<>
|
||||
<ModelsChartPreferences
|
||||
preferences={chartPreferences}
|
||||
onPreferencesChange={handleChartPreferencesChange}
|
||||
/>
|
||||
<ModelsFilter
|
||||
preferences={chartPreferences}
|
||||
onFilterChange={handleFilterChange}
|
||||
onReset={handleResetFilters}
|
||||
/>
|
||||
</>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<SectionPageLayout>
|
||||
@@ -153,26 +190,29 @@ export function Dashboard() {
|
||||
<SectionPageLayout.Description>
|
||||
{t(meta.descriptionKey)}
|
||||
</SectionPageLayout.Description>
|
||||
{activeSection === 'models' && (
|
||||
<SectionPageLayout.Actions>
|
||||
<ModelsFilter
|
||||
onFilterChange={handleFilterChange}
|
||||
onReset={handleResetFilters}
|
||||
/>
|
||||
</SectionPageLayout.Actions>
|
||||
)}
|
||||
<SectionPageLayout.Content>
|
||||
<div className='space-y-4'>
|
||||
{showSectionTabs && (
|
||||
<Tabs value={activeSection} onValueChange={handleSectionChange}>
|
||||
<TabsList className='h-auto max-w-full flex-wrap justify-start'>
|
||||
{visibleSections.map((section) => (
|
||||
<TabsTrigger key={section} value={section}>
|
||||
{t(SECTION_META[section].titleKey)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{activeSection !== 'overview' && (
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
{showSectionTabs ? (
|
||||
<Tabs value={activeSection} onValueChange={handleSectionChange}>
|
||||
<TabsList className='h-auto max-w-full flex-wrap justify-start'>
|
||||
{visibleSections.map((section) => (
|
||||
<TabsTrigger key={section} value={section}>
|
||||
{t(SECTION_META[section].titleKey)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
{modelActions != null && (
|
||||
<div className='flex shrink-0 flex-wrap items-center gap-2'>
|
||||
{modelActions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{activeSection === 'overview' && (
|
||||
<>
|
||||
@@ -208,6 +248,9 @@ export function Dashboard() {
|
||||
<LazyConsumptionDistributionChart
|
||||
data={modelData}
|
||||
loading={dataLoading}
|
||||
defaultChartType={
|
||||
chartPreferences.consumptionDistributionChart
|
||||
}
|
||||
timeGranularity={
|
||||
modelFilters.time_granularity || DEFAULT_TIME_GRANULARITY
|
||||
}
|
||||
@@ -219,6 +262,7 @@ export function Dashboard() {
|
||||
<LazyModelCharts
|
||||
data={modelData}
|
||||
loading={dataLoading}
|
||||
defaultChartTab={chartPreferences.modelAnalyticsChart}
|
||||
timeGranularity={
|
||||
modelFilters.time_granularity || DEFAULT_TIME_GRANULARITY
|
||||
}
|
||||
|
||||
+12
-2
@@ -910,8 +910,18 @@ export function processUserChartData(
|
||||
},
|
||||
},
|
||||
},
|
||||
area: { style: { fillOpacity: 0.15 } },
|
||||
line: { style: { lineWidth: 2 } },
|
||||
area: {
|
||||
style: {
|
||||
fillOpacity: 0.15,
|
||||
curveType: 'monotone',
|
||||
},
|
||||
},
|
||||
line: {
|
||||
style: {
|
||||
lineWidth: 2,
|
||||
curveType: 'monotone',
|
||||
},
|
||||
},
|
||||
point: { visible: false },
|
||||
color: { specified: userColorMap },
|
||||
background: { fill: 'transparent' },
|
||||
|
||||
+102
-4
@@ -1,9 +1,46 @@
|
||||
import type { TimeGranularity } from '@/lib/time'
|
||||
import { getNormalizedDateRange } from '@/lib/time'
|
||||
import {
|
||||
DASHBOARD_CHART_PREFERENCES_STORAGE_KEY,
|
||||
DEFAULT_DASHBOARD_CHART_PREFERENCES,
|
||||
DEFAULT_TIME_GRANULARITY,
|
||||
EMPTY_DASHBOARD_FILTERS,
|
||||
TIME_GRANULARITY_STORAGE_KEY,
|
||||
TIME_RANGE_PRESETS,
|
||||
TIME_RANGE_BY_GRANULARITY,
|
||||
} from '@/features/dashboard/constants'
|
||||
import type {
|
||||
ConsumptionDistributionChartType,
|
||||
DashboardChartPreferences,
|
||||
DashboardFilters,
|
||||
ModelAnalyticsChartTab,
|
||||
} from '@/features/dashboard/types'
|
||||
|
||||
function isTimeGranularity(value: unknown): value is TimeGranularity {
|
||||
return value === 'hour' || value === 'day' || value === 'week'
|
||||
}
|
||||
|
||||
function getLegacySavedGranularity(): TimeGranularity {
|
||||
if (typeof window === 'undefined') return DEFAULT_TIME_GRANULARITY
|
||||
const saved = localStorage.getItem(TIME_GRANULARITY_STORAGE_KEY)
|
||||
return isTimeGranularity(saved) ? saved : DEFAULT_TIME_GRANULARITY
|
||||
}
|
||||
|
||||
function isConsumptionDistributionChartType(
|
||||
value: unknown
|
||||
): value is ConsumptionDistributionChartType {
|
||||
return value === 'bar' || value === 'area'
|
||||
}
|
||||
|
||||
function isModelAnalyticsChartTab(
|
||||
value: unknown
|
||||
): value is ModelAnalyticsChartTab {
|
||||
return value === 'trend' || value === 'proportion' || value === 'top'
|
||||
}
|
||||
|
||||
function isTimeRangePresetDays(value: unknown): value is number {
|
||||
return TIME_RANGE_PRESETS.some((preset) => preset.days === value)
|
||||
}
|
||||
|
||||
export function cleanFilters<T extends Record<string, unknown>>(
|
||||
filters: T
|
||||
@@ -25,20 +62,81 @@ export function getSavedGranularity(
|
||||
override?: TimeGranularity
|
||||
): TimeGranularity {
|
||||
if (override) return override
|
||||
if (typeof window === 'undefined') return DEFAULT_TIME_GRANULARITY
|
||||
const saved = localStorage.getItem(TIME_GRANULARITY_STORAGE_KEY)
|
||||
if (saved === 'hour' || saved === 'day' || saved === 'week') return saved
|
||||
return DEFAULT_TIME_GRANULARITY
|
||||
return getSavedChartPreferences().defaultTimeGranularity
|
||||
}
|
||||
|
||||
export function saveGranularity(granularity: TimeGranularity): void {
|
||||
if (typeof window === 'undefined') return
|
||||
saveChartPreferences({
|
||||
...getSavedChartPreferences(),
|
||||
defaultTimeGranularity: granularity,
|
||||
})
|
||||
localStorage.setItem(TIME_GRANULARITY_STORAGE_KEY, granularity)
|
||||
}
|
||||
|
||||
export function getSavedChartPreferences(): DashboardChartPreferences {
|
||||
if (typeof window === 'undefined') return DEFAULT_DASHBOARD_CHART_PREFERENCES
|
||||
|
||||
const fallbackPreferences = {
|
||||
...DEFAULT_DASHBOARD_CHART_PREFERENCES,
|
||||
defaultTimeGranularity: getLegacySavedGranularity(),
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(DASHBOARD_CHART_PREFERENCES_STORAGE_KEY)
|
||||
if (!raw) return fallbackPreferences
|
||||
|
||||
const parsed = JSON.parse(raw) as Partial<DashboardChartPreferences>
|
||||
return {
|
||||
consumptionDistributionChart: isConsumptionDistributionChartType(
|
||||
parsed.consumptionDistributionChart
|
||||
)
|
||||
? parsed.consumptionDistributionChart
|
||||
: fallbackPreferences.consumptionDistributionChart,
|
||||
modelAnalyticsChart: isModelAnalyticsChartTab(parsed.modelAnalyticsChart)
|
||||
? parsed.modelAnalyticsChart
|
||||
: fallbackPreferences.modelAnalyticsChart,
|
||||
defaultTimeRangeDays: isTimeRangePresetDays(parsed.defaultTimeRangeDays)
|
||||
? parsed.defaultTimeRangeDays
|
||||
: fallbackPreferences.defaultTimeRangeDays,
|
||||
defaultTimeGranularity: isTimeGranularity(
|
||||
parsed.defaultTimeGranularity
|
||||
)
|
||||
? parsed.defaultTimeGranularity
|
||||
: fallbackPreferences.defaultTimeGranularity,
|
||||
}
|
||||
} catch {
|
||||
return fallbackPreferences
|
||||
}
|
||||
}
|
||||
|
||||
export function saveChartPreferences(
|
||||
preferences: DashboardChartPreferences
|
||||
): void {
|
||||
if (typeof window === 'undefined') return
|
||||
localStorage.setItem(
|
||||
DASHBOARD_CHART_PREFERENCES_STORAGE_KEY,
|
||||
JSON.stringify(preferences)
|
||||
)
|
||||
}
|
||||
|
||||
export function getDefaultDays(granularity?: TimeGranularity): number {
|
||||
if (!granularity) return getSavedChartPreferences().defaultTimeRangeDays
|
||||
return TIME_RANGE_BY_GRANULARITY[getSavedGranularity(granularity)]
|
||||
}
|
||||
|
||||
export function buildDefaultDashboardFilters(
|
||||
preferences: DashboardChartPreferences = getSavedChartPreferences()
|
||||
): DashboardFilters {
|
||||
const { start, end } = getNormalizedDateRange(preferences.defaultTimeRangeDays)
|
||||
return {
|
||||
...EMPTY_DASHBOARD_FILTERS,
|
||||
start_timestamp: start,
|
||||
end_timestamp: end,
|
||||
time_granularity: preferences.defaultTimeGranularity,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildQueryParams(
|
||||
timeRange: { start_timestamp: number; end_timestamp: number },
|
||||
filters?: { time_granularity?: TimeGranularity; username?: string }
|
||||
|
||||
@@ -4,6 +4,9 @@ export {
|
||||
getSavedGranularity,
|
||||
saveGranularity,
|
||||
getDefaultDays,
|
||||
getSavedChartPreferences,
|
||||
saveChartPreferences,
|
||||
buildDefaultDashboardFilters,
|
||||
} from './filters'
|
||||
export {
|
||||
getLatencyColorClass,
|
||||
|
||||
+11
@@ -42,6 +42,17 @@ export interface DashboardFilters {
|
||||
username?: string
|
||||
}
|
||||
|
||||
export type ConsumptionDistributionChartType = 'bar' | 'area'
|
||||
|
||||
export type ModelAnalyticsChartTab = 'trend' | 'proportion' | 'top'
|
||||
|
||||
export interface DashboardChartPreferences {
|
||||
consumptionDistributionChart: ConsumptionDistributionChartType
|
||||
modelAnalyticsChart: ModelAnalyticsChartTab
|
||||
defaultTimeRangeDays: number
|
||||
defaultTimeGranularity: TimeGranularity
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API Info Types
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user