Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
8bff6910
authored
Apr 30, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(ui): add reusable dashboard and log controls
parent
22fd1741
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
473 additions
and
0 deletions
+473
-0
web/default/src/components/group-badge.tsx
+88
-0
web/default/src/features/dashboard/components/models/models-chart-preferences.tsx
+188
-0
web/default/src/features/usage-logs/components/task-logs-filter-bar.tsx
+197
-0
No files found.
web/default/src/components/group-badge.tsx
0 → 100644
View file @
8bff6910
import
{
useTranslation
}
from
'react-i18next'
import
{
cn
}
from
'@/lib/utils'
import
{
StatusBadge
,
type
StatusBadgeProps
}
from
'./status-badge'
type
GroupBadgeProps
=
Omit
<
StatusBadgeProps
,
'autoColor'
|
'label'
|
'variant'
>
&
{
group
?:
string
|
null
label
?:
string
ratio
?:
number
|
null
}
function
getGroupRatioClassName
(
ratio
:
number
):
string
{
if
(
ratio
>
1
)
{
return
'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/60 dark:bg-amber-950/40 dark:text-amber-300'
}
if
(
ratio
<
1
)
{
return
'border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-900/60 dark:bg-sky-950/40 dark:text-sky-300'
}
return
'border-border bg-muted text-muted-foreground'
}
function
getGroupLabel
(
params
:
{
labelOverride
?:
string
groupName
?:
string
isAutoGroup
:
boolean
isEmptyGroup
:
boolean
t
:
(
key
:
string
)
=>
string
}):
string
{
if
(
params
.
labelOverride
)
return
params
.
labelOverride
if
(
params
.
isEmptyGroup
)
return
params
.
t
(
'User Group'
)
if
(
params
.
isAutoGroup
)
return
params
.
t
(
'Auto'
)
return
params
.
groupName
??
''
}
export
function
GroupBadge
(
props
:
GroupBadgeProps
)
{
const
{
t
}
=
useTranslation
()
const
{
group
,
label
:
labelOverride
,
ratio
,
copyable
=
false
,
showDot
,
...
badgeProps
}
=
props
const
groupName
=
group
?.
trim
()
const
isAutoGroup
=
groupName
===
'auto'
const
isEmptyGroup
=
!
groupName
const
isSpecialGroup
=
isAutoGroup
||
isEmptyGroup
const
label
=
getGroupLabel
({
labelOverride
,
groupName
,
isAutoGroup
,
isEmptyGroup
,
t
,
})
const
badge
=
(
<
StatusBadge
{
...
badgeProps
}
copyable=
{
copyable
}
label=
{
label
}
showDot=
{
showDot
??
(
isSpecialGroup
?
false
:
undefined
)
}
variant=
{
isSpecialGroup
?
'neutral'
:
undefined
}
autoColor=
{
isSpecialGroup
?
undefined
:
groupName
}
/>
)
if
(
ratio
==
null
)
{
return
badge
}
return
(
<
span
className=
'inline-flex items-center gap-2 text-xs'
>
{
badge
}
<
span
className=
{
cn
(
'inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 font-mono text-[11px] leading-none tabular-nums'
,
getGroupRatioClassName
(
ratio
)
)
}
>
<
span
className=
'size-1 rounded-full bg-current opacity-60'
/>
<
span
>
{
ratio
}
x
</
span
>
</
span
>
</
span
>
)
}
web/default/src/features/dashboard/components/models/models-chart-preferences.tsx
0 → 100644
View file @
8bff6910
import
{
useEffect
,
useState
}
from
'react'
import
{
Save
,
Settings2
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
Button
}
from
'@/components/ui/button'
import
{
Dialog
,
DialogContent
,
DialogDescription
,
DialogFooter
,
DialogHeader
,
DialogTitle
,
DialogTrigger
,
}
from
'@/components/ui/dialog'
import
{
Label
}
from
'@/components/ui/label'
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
,
}
from
'@/components/ui/select'
import
{
CONSUMPTION_DISTRIBUTION_CHART_OPTIONS
,
MODEL_ANALYTICS_CHART_OPTIONS
,
TIME_GRANULARITY_OPTIONS
,
TIME_RANGE_PRESETS
,
}
from
'@/features/dashboard/constants'
import
type
{
ConsumptionDistributionChartType
,
DashboardChartPreferences
,
ModelAnalyticsChartTab
,
}
from
'@/features/dashboard/types'
import
type
{
TimeGranularity
}
from
'@/lib/time'
interface
ModelsChartPreferencesProps
{
preferences
:
DashboardChartPreferences
onPreferencesChange
:
(
preferences
:
DashboardChartPreferences
)
=>
void
}
export
function
ModelsChartPreferences
(
props
:
ModelsChartPreferencesProps
)
{
const
{
t
}
=
useTranslation
()
const
[
open
,
setOpen
]
=
useState
(
false
)
const
[
draft
,
setDraft
]
=
useState
<
DashboardChartPreferences
>
(
props
.
preferences
)
useEffect
(()
=>
{
if
(
open
)
setDraft
(
props
.
preferences
)
},
[
open
,
props
.
preferences
])
const
handleSave
=
()
=>
{
props
.
onPreferencesChange
(
draft
)
setOpen
(
false
)
}
return
(
<
Dialog
open=
{
open
}
onOpenChange=
{
setOpen
}
>
<
DialogTrigger
asChild
>
<
Button
variant=
'outline'
size=
'sm'
>
<
Settings2
className=
'mr-2 h-4 w-4'
/>
{
t
(
'Preferences'
)
}
</
Button
>
</
DialogTrigger
>
<
DialogContent
className=
'sm:max-w-md'
>
<
DialogHeader
>
<
DialogTitle
>
{
t
(
'Dashboard Preferences'
)
}
</
DialogTitle
>
<
DialogDescription
>
{
t
(
'Choose the default charts, range, and time granularity for model analytics.'
)
}
</
DialogDescription
>
</
DialogHeader
>
<
div
className=
'grid gap-4 py-2'
>
<
div
className=
'grid gap-2'
>
<
Label
htmlFor=
'default-time-range'
>
{
t
(
'Default range'
)
}
</
Label
>
<
Select
value=
{
String
(
draft
.
defaultTimeRangeDays
)
}
onValueChange=
{
(
value
)
=>
setDraft
((
prev
)
=>
({
...
prev
,
defaultTimeRangeDays
:
Number
(
value
),
}))
}
>
<
SelectTrigger
id=
'default-time-range'
>
<
SelectValue
placeholder=
{
t
(
'Select default range'
)
}
/>
</
SelectTrigger
>
<
SelectContent
>
{
TIME_RANGE_PRESETS
.
map
((
option
)
=>
(
<
SelectItem
key=
{
option
.
days
}
value=
{
String
(
option
.
days
)
}
>
{
t
(
option
.
label
)
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
</
div
>
<
div
className=
'grid gap-2'
>
<
Label
htmlFor=
'default-time-granularity'
>
{
t
(
'Default time granularity'
)
}
</
Label
>
<
Select
value=
{
draft
.
defaultTimeGranularity
}
onValueChange=
{
(
value
)
=>
setDraft
((
prev
)
=>
({
...
prev
,
defaultTimeGranularity
:
value
as
TimeGranularity
,
}))
}
>
<
SelectTrigger
id=
'default-time-granularity'
>
<
SelectValue
placeholder=
{
t
(
'Select time granularity'
)
}
/>
</
SelectTrigger
>
<
SelectContent
>
{
TIME_GRANULARITY_OPTIONS
.
map
((
option
)
=>
(
<
SelectItem
key=
{
option
.
value
}
value=
{
option
.
value
}
>
{
t
(
option
.
label
)
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
</
div
>
<
div
className=
'grid gap-2'
>
<
Label
htmlFor=
'consumption-distribution-chart'
>
{
t
(
'Default consumption chart'
)
}
</
Label
>
<
Select
value=
{
draft
.
consumptionDistributionChart
}
onValueChange=
{
(
value
)
=>
setDraft
((
prev
)
=>
({
...
prev
,
consumptionDistributionChart
:
value
as
ConsumptionDistributionChartType
,
}))
}
>
<
SelectTrigger
id=
'consumption-distribution-chart'
>
<
SelectValue
placeholder=
{
t
(
'Select default chart'
)
}
/>
</
SelectTrigger
>
<
SelectContent
>
{
CONSUMPTION_DISTRIBUTION_CHART_OPTIONS
.
map
((
option
)
=>
(
<
SelectItem
key=
{
option
.
value
}
value=
{
option
.
value
}
>
{
t
(
option
.
labelKey
)
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
</
div
>
<
div
className=
'grid gap-2'
>
<
Label
htmlFor=
'model-analytics-chart'
>
{
t
(
'Default model call chart'
)
}
</
Label
>
<
Select
value=
{
draft
.
modelAnalyticsChart
}
onValueChange=
{
(
value
)
=>
setDraft
((
prev
)
=>
({
...
prev
,
modelAnalyticsChart
:
value
as
ModelAnalyticsChartTab
,
}))
}
>
<
SelectTrigger
id=
'model-analytics-chart'
>
<
SelectValue
placeholder=
{
t
(
'Select default chart'
)
}
/>
</
SelectTrigger
>
<
SelectContent
>
{
MODEL_ANALYTICS_CHART_OPTIONS
.
map
((
option
)
=>
(
<
SelectItem
key=
{
option
.
value
}
value=
{
option
.
value
}
>
{
t
(
option
.
labelKey
)
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
</
div
>
</
div
>
<
DialogFooter
>
<
Button
onClick=
{
handleSave
}
type=
'button'
>
<
Save
className=
'mr-2 h-4 w-4'
/>
{
t
(
'Save Preferences'
)
}
</
Button
>
</
DialogFooter
>
</
DialogContent
>
</
Dialog
>
)
}
web/default/src/features/usage-logs/components/task-logs-filter-bar.tsx
0 → 100644
View file @
8bff6910
import
{
useState
,
useEffect
,
useCallback
,
type
ReactNode
}
from
'react'
import
{
useNavigate
,
getRouteApi
}
from
'@tanstack/react-router'
import
{
useQueryClient
,
useIsFetching
}
from
'@tanstack/react-query'
import
{
Loader2
,
RotateCcw
,
Search
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useIsAdmin
}
from
'@/hooks/use-admin'
import
{
Button
}
from
'@/components/ui/button'
import
{
Input
}
from
'@/components/ui/input'
import
{
buildSearchParams
}
from
'../lib/filter'
import
{
getDefaultTimeRange
}
from
'../lib/utils'
import
type
{
DrawingLogFilters
,
LogCategory
,
TaskLogFilters
}
from
'../types'
import
{
CompactDateTimeRangePicker
}
from
'./compact-date-time-range-picker'
const
route
=
getRouteApi
(
'/_authenticated/usage-logs/$section'
)
type
TaskLikeLogCategory
=
Extract
<
LogCategory
,
'drawing'
|
'task'
>
type
TaskLogsFilters
=
DrawingLogFilters
|
TaskLogFilters
interface
TaskLogsFilterBarProps
{
logCategory
:
TaskLikeLogCategory
viewOptions
?:
ReactNode
}
function
getFilterPlaceholder
(
_logCategory
:
TaskLikeLogCategory
):
string
{
return
'Filter by task ID'
}
function
getFilterValue
(
filters
:
TaskLogsFilters
,
logCategory
:
TaskLikeLogCategory
):
string
{
if
(
logCategory
===
'drawing'
)
{
return
(
filters
as
DrawingLogFilters
).
mjId
||
''
}
return
(
filters
as
TaskLogFilters
).
taskId
||
''
}
function
setFilterValue
(
filters
:
TaskLogsFilters
,
logCategory
:
TaskLikeLogCategory
,
value
:
string
):
TaskLogsFilters
{
if
(
logCategory
===
'drawing'
)
{
return
{
...
filters
,
mjId
:
value
}
}
return
{
...
filters
,
taskId
:
value
}
}
export
function
TaskLogsFilterBar
(
props
:
TaskLogsFilterBarProps
)
{
const
{
t
}
=
useTranslation
()
const
navigate
=
useNavigate
()
const
queryClient
=
useQueryClient
()
const
searchParams
=
route
.
useSearch
()
const
isAdmin
=
useIsAdmin
()
const
fetchingLogs
=
useIsFetching
({
queryKey
:
[
'logs'
]
})
const
[
filters
,
setFilters
]
=
useState
<
TaskLogsFilters
>
(()
=>
{
const
{
start
,
end
}
=
getDefaultTimeRange
()
return
{
startTime
:
start
,
endTime
:
end
}
})
useEffect
(()
=>
{
const
{
start
,
end
}
=
getDefaultTimeRange
()
const
baseFilters
=
{
startTime
:
searchParams
.
startTime
?
new
Date
(
searchParams
.
startTime
)
:
start
,
endTime
:
searchParams
.
endTime
?
new
Date
(
searchParams
.
endTime
)
:
end
,
...(
searchParams
.
channel
?
{
channel
:
String
(
searchParams
.
channel
)
}
:
{}),
}
const
next
:
TaskLogsFilters
=
props
.
logCategory
===
'drawing'
?
{
...
baseFilters
,
...(
searchParams
.
filter
?
{
mjId
:
searchParams
.
filter
}
:
{}),
}
:
{
...
baseFilters
,
...(
searchParams
.
filter
?
{
taskId
:
searchParams
.
filter
}
:
{}),
}
setFilters
(
next
)
},
[
props
.
logCategory
,
searchParams
.
startTime
,
searchParams
.
endTime
,
searchParams
.
channel
,
searchParams
.
filter
,
])
const
handleChange
=
useCallback
(
(
field
:
keyof
TaskLogsFilters
,
value
:
Date
|
string
|
undefined
)
=>
{
setFilters
((
prev
)
=>
({
...
prev
,
[
field
]:
value
}))
},
[]
)
const
handleApply
=
useCallback
(()
=>
{
const
filterParams
=
buildSearchParams
(
filters
,
props
.
logCategory
)
navigate
({
to
:
'/usage-logs/$section'
,
params
:
{
section
:
props
.
logCategory
},
search
:
{
...
filterParams
,
page
:
1
,
},
})
queryClient
.
invalidateQueries
({
queryKey
:
[
'logs'
]
})
},
[
filters
,
navigate
,
props
.
logCategory
,
queryClient
])
const
handleReset
=
useCallback
(()
=>
{
const
{
start
,
end
}
=
getDefaultTimeRange
()
const
resetFilters
:
TaskLogsFilters
=
{
startTime
:
start
,
endTime
:
end
}
setFilters
(
resetFilters
)
navigate
({
to
:
'/usage-logs/$section'
,
params
:
{
section
:
props
.
logCategory
},
search
:
{
page
:
1
,
startTime
:
start
.
getTime
(),
endTime
:
end
.
getTime
(),
},
})
queryClient
.
invalidateQueries
({
queryKey
:
[
'logs'
]
})
},
[
navigate
,
props
.
logCategory
,
queryClient
])
const
handleKeyDown
=
useCallback
(
(
e
:
React
.
KeyboardEvent
)
=>
{
if
(
e
.
key
===
'Enter'
)
handleApply
()
},
[
handleApply
]
)
const
handleFilterChange
=
useCallback
(
(
value
:
string
)
=>
{
setFilters
((
prev
)
=>
setFilterValue
(
prev
,
props
.
logCategory
,
value
))
},
[
props
.
logCategory
]
)
return
(
<
div
className=
'space-y-3'
>
<
div
className=
'grid grid-cols-2 gap-2 lg:grid-cols-[minmax(280px,2fr)_minmax(180px,1fr)_minmax(120px,0.8fr)_auto]'
>
<
CompactDateTimeRangePicker
start=
{
filters
.
startTime
}
end=
{
filters
.
endTime
}
onChange=
{
({
start
,
end
})
=>
{
handleChange
(
'startTime'
,
start
)
handleChange
(
'endTime'
,
end
)
}
}
className=
'col-span-2 lg:col-span-1'
/>
<
Input
aria
-
label=
{
t
(
'Task ID'
)
}
placeholder=
{
t
(
getFilterPlaceholder
(
props
.
logCategory
))
}
value=
{
getFilterValue
(
filters
,
props
.
logCategory
)
}
onChange=
{
(
e
)
=>
handleFilterChange
(
e
.
target
.
value
)
}
onKeyDown=
{
handleKeyDown
}
className=
'h-9'
/>
{
isAdmin
&&
(
<
Input
placeholder=
{
t
(
'Channel ID'
)
}
value=
{
filters
.
channel
||
''
}
onChange=
{
(
e
)
=>
handleChange
(
'channel'
,
e
.
target
.
value
)
}
onKeyDown=
{
handleKeyDown
}
className=
'h-9'
/>
)
}
<
div
className=
'col-span-2 flex shrink-0 items-center justify-end gap-2 lg:col-span-1'
>
<
Button
variant=
'outline'
size=
'sm'
className=
'h-8'
onClick=
{
handleReset
}
>
<
RotateCcw
className=
'size-3.5'
/>
{
t
(
'Reset'
)
}
</
Button
>
<
Button
size=
'sm'
className=
'h-8'
onClick=
{
handleApply
}
disabled=
{
fetchingLogs
>
0
}
>
{
fetchingLogs
>
0
?
(
<
Loader2
className=
'size-3.5 animate-spin'
/>
)
:
(
<
Search
className=
'size-3.5'
/>
)
}
{
t
(
'Search'
)
}
</
Button
>
{
props
.
viewOptions
}
</
div
>
</
div
>
</
div
>
)
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment