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
521cebf5
authored
Sep 06, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(dashboard): simplify completed setup guide
parent
3f8a50cf
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
298 additions
and
18 deletions
+298
-18
web/src/features/dashboard/components/overview/__tests__/setup-guide.test.tsx
+241
-0
web/src/features/dashboard/components/overview/overview-dashboard.tsx
+53
-15
web/src/features/dashboard/index.tsx
+4
-3
No files found.
web/src/features/dashboard/components/overview/__tests__/setup-guide.test.tsx
0 → 100644
View file @
521cebf5
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
{
QueryClient
,
QueryClientProvider
}
from
'@tanstack/react-query'
import
{
createMemoryHistory
,
createRootRoute
,
createRouter
,
RouterProvider
,
}
from
'@tanstack/react-router'
import
{
act
,
cleanup
,
render
,
screen
,
waitFor
}
from
'@testing-library/react'
import
userEvent
from
'@testing-library/user-event'
import
{
afterEach
,
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'vitest'
import
{
api
}
from
'@/lib/api'
import
{
useAuthStore
}
from
'@/stores/auth-store'
import
{
useSystemConfigStore
}
from
'@/stores/system-config-store'
import
{
OverviewDashboard
}
from
'../overview-dashboard'
const
storageKey
=
'dashboard_overview_setup_guide_expanded'
let
client
:
QueryClient
let
keyLookupError
:
Error
|
null
beforeEach
(()
=>
{
window
.
localStorage
.
clear
()
useSystemConfigStore
.
setState
(
useSystemConfigStore
.
getInitialState
(),
true
)
useAuthStore
.
getState
().
auth
.
setUser
({
id
:
1
,
username
:
'dashboard-user'
,
role
:
1
,
quota
:
1000000
,
used_quota
:
1000
,
request_count
:
1
,
})
client
=
new
QueryClient
({
defaultOptions
:
{
queries
:
{
retry
:
false
}
},
})
keyLookupError
=
null
vi
.
spyOn
(
api
,
'get'
).
mockImplementation
(
async
(
url
)
=>
{
switch
(
url
)
{
case
'/api/token/?p=1&size=10'
:
if
(
keyLookupError
)
throw
keyLookupError
return
{
data
:
{
success
:
true
,
data
:
{
items
:
[{
id
:
1
,
name
:
'App key'
,
key
:
'masked'
,
status
:
1
}],
},
},
}
case
'/api/status'
:
return
{
data
:
{
data
:
{
api_info_enabled
:
false
,
announcements_enabled
:
false
,
faq_enabled
:
false
,
uptime_kuma_enabled
:
false
,
},
},
}
case
'/api/user/models'
:
return
{
data
:
{
success
:
true
,
data
:
[
'gpt-4o-mini'
]
}
}
case
'/api/data/self'
:
return
{
data
:
{
success
:
true
,
data
:
[]
}
}
default
:
throw
new
Error
(
`Unexpected dashboard request:
${
url
}
`
)
}
})
})
afterEach
(()
=>
{
cleanup
()
client
.
clear
()
useAuthStore
.
setState
(
useAuthStore
.
getInitialState
(),
true
)
useSystemConfigStore
.
setState
(
useSystemConfigStore
.
getInitialState
(),
true
)
window
.
localStorage
.
clear
()
})
async
function
renderOverview
()
{
const
router
=
createRouter
({
routeTree
:
createRootRoute
({
component
:
OverviewDashboard
}),
history
:
createMemoryHistory
({
initialEntries
:
[
'/'
]
}),
})
await
router
.
load
()
return
render
(
<
QueryClientProvider
client=
{
client
}
>
<
RouterProvider
router=
{
router
}
/>
</
QueryClientProvider
>
)
}
describe
(
'overview setup guide'
,
()
=>
{
it
(
'shows usage first and only a header entry when setup is complete'
,
async
()
=>
{
await
renderOverview
()
const
toggle
=
await
screen
.
findByRole
(
'button'
,
{
name
:
'Setup guide'
})
expect
(
toggle
).
toHaveAttribute
(
'aria-expanded'
,
'false'
)
expect
(
screen
.
getAllByRole
(
'heading'
).
map
((
heading
)
=>
heading
.
textContent
)
).
toEqual
([
'Overview'
,
'Usage at a glance'
])
expect
(
screen
.
queryByText
(
'Setup guide complete'
)).
not
.
toBeInTheDocument
()
expect
(
screen
.
queryByText
(
'Setup progress: 3/3'
)).
not
.
toBeInTheDocument
()
for
(
const
name
of
[
'API Keys'
,
'Channels'
,
'Usage Logs'
,
'Pricing'
])
{
expect
(
screen
.
queryByRole
(
'button'
,
{
name
})).
not
.
toBeInTheDocument
()
}
const
panel
=
document
.
getElementById
(
toggle
.
getAttribute
(
'aria-controls'
)
??
''
)
expect
(
panel
).
toBeInTheDocument
()
expect
(
panel
).
not
.
toBeVisible
()
})
it
(
'toggles the completed guide with the keyboard and restores focus after hiding it'
,
async
()
=>
{
const
user
=
userEvent
.
setup
()
await
renderOverview
()
const
toggle
=
await
screen
.
findByRole
(
'button'
,
{
name
:
'Setup guide'
})
await
user
.
tab
()
expect
(
toggle
).
toHaveFocus
()
await
user
.
keyboard
(
'{Enter}'
)
expect
(
toggle
).
toHaveAttribute
(
'aria-expanded'
,
'true'
)
expect
(
document
.
getElementById
(
toggle
.
getAttribute
(
'aria-controls'
)
??
''
)
).
toBeVisible
()
expect
(
screen
.
getByRole
(
'heading'
,
{
name
:
'Build on your API gateway in minutes'
,
})
).
toBeVisible
()
await
waitFor
(()
=>
expect
(
screen
.
getByRole
(
'button'
,
{
name
:
/^API Keys/
})).
toBeVisible
()
)
await
user
.
keyboard
(
' '
)
expect
(
toggle
).
toHaveAttribute
(
'aria-expanded'
,
'false'
)
expect
(
toggle
).
toHaveFocus
()
await
user
.
click
(
toggle
)
await
user
.
click
(
screen
.
getByRole
(
'button'
,
{
name
:
'Hide setup guide'
}))
expect
(
toggle
).
toHaveAttribute
(
'aria-expanded'
,
'false'
)
expect
(
toggle
).
toHaveFocus
()
})
it
(
'restores the completed guide preference after remounting'
,
async
()
=>
{
const
user
=
userEvent
.
setup
()
const
first
=
await
renderOverview
()
await
user
.
click
(
await
screen
.
findByRole
(
'button'
,
{
name
:
'Setup guide'
}))
first
.
unmount
()
const
second
=
await
renderOverview
()
expect
(
await
screen
.
findByRole
(
'button'
,
{
name
:
'Setup guide'
})
).
toHaveAttribute
(
'aria-expanded'
,
'true'
)
await
user
.
click
(
screen
.
getByRole
(
'button'
,
{
name
:
'Hide setup guide'
}))
second
.
unmount
()
await
renderOverview
()
expect
(
await
screen
.
findByRole
(
'button'
,
{
name
:
'Setup guide'
})
).
toHaveAttribute
(
'aria-expanded'
,
'false'
)
expect
(
screen
.
queryByText
(
'Setup guide complete'
)).
not
.
toBeInTheDocument
()
})
it
(
'keeps the existing progress banner when an incomplete guide is manually collapsed'
,
async
()
=>
{
const
user
=
userEvent
.
setup
()
useAuthStore
.
getState
()
.
auth
.
setUser
({
id
:
1
,
username
:
'new-user'
,
role
:
1
})
await
renderOverview
()
await
user
.
click
(
await
screen
.
findByRole
(
'button'
,
{
name
:
'Hide setup guide'
})
)
expect
(
screen
.
getByText
(
'Setup progress: 1/3'
)).
toBeVisible
()
expect
(
screen
.
getByText
(
'Setup guide is collapsed. Expand it anytime.'
)
).
toBeVisible
()
expect
(
screen
.
getByRole
(
'button'
,
{
name
:
'API Keys'
})).
toBeVisible
()
expect
(
screen
.
queryByRole
(
'button'
,
{
name
:
'Setup guide'
})
).
not
.
toBeInTheDocument
()
await
user
.
click
(
screen
.
getByRole
(
'button'
,
{
name
:
'Show setup guide'
}))
expect
(
screen
.
getByRole
(
'button'
,
{
name
:
'Hide setup guide'
})
).
toBeVisible
()
})
it
(
'removes the collapsed progress banner when the remaining setup step completes'
,
async
()
=>
{
useAuthStore
.
getState
().
auth
.
setUser
({
id
:
1
,
username
:
'dashboard-user'
,
role
:
1
,
quota
:
1000000
,
})
window
.
localStorage
.
setItem
(
storageKey
,
'collapsed'
)
await
renderOverview
()
expect
(
await
screen
.
findByText
(
'Setup progress: 2/3'
)).
toBeVisible
()
act
(()
=>
{
useAuthStore
.
getState
().
auth
.
setUser
({
id
:
1
,
username
:
'dashboard-user'
,
role
:
1
,
quota
:
1000000
,
request_count
:
1
,
})
})
expect
(
await
screen
.
findByRole
(
'button'
,
{
name
:
'Setup guide'
})
).
toHaveAttribute
(
'aria-expanded'
,
'false'
)
expect
(
screen
.
queryByText
(
/Setup progress:/
)).
not
.
toBeInTheDocument
()
})
it
(
'does not show a completed setup entry when the key lookup fails'
,
async
()
=>
{
keyLookupError
=
new
Error
(
'Key lookup unavailable'
)
await
renderOverview
()
expect
(
await
screen
.
findByRole
(
'button'
,
{
name
:
'Hide setup guide'
})
).
toBeVisible
()
expect
(
screen
.
queryByRole
(
'button'
,
{
name
:
'Setup guide'
})
).
not
.
toBeInTheDocument
()
})
})
web/src/features/dashboard/components/overview/overview-dashboard.tsx
View file @
521cebf5
...
@@ -37,10 +37,11 @@ import {
...
@@ -37,10 +37,11 @@ import {
type
LucideIcon
,
type
LucideIcon
,
}
from
'lucide-react'
}
from
'lucide-react'
import
{
motion
,
useReducedMotion
}
from
'motion/react'
import
{
motion
,
useReducedMotion
}
from
'motion/react'
import
{
use
Memo
,
useState
}
from
'react'
import
{
use
Id
,
useMemo
,
useRef
,
useState
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
toast
}
from
'sonner'
import
{
toast
}
from
'sonner'
import
{
SectionPageLayout
}
from
'@/components/layout'
import
{
import
{
CardStaggerContainer
,
CardStaggerContainer
,
CardStaggerItem
,
CardStaggerItem
,
...
@@ -457,6 +458,8 @@ function CompactQuickAction(props: { action: QuickAction }) {
...
@@ -457,6 +458,8 @@ function CompactQuickAction(props: { action: QuickAction }) {
export
function
OverviewDashboard
()
{
export
function
OverviewDashboard
()
{
const
{
t
}
=
useTranslation
()
const
{
t
}
=
useTranslation
()
const
setupGuideId
=
useId
()
const
setupGuideToggleRef
=
useRef
<
HTMLButtonElement
>
(
null
)
const
user
=
useAuthStore
((
state
)
=>
state
.
auth
.
user
)
const
user
=
useAuthStore
((
state
)
=>
state
.
auth
.
user
)
const
{
items
:
apiInfoItems
}
=
useApiInfo
()
const
{
items
:
apiInfoItems
}
=
useApiInfo
()
const
{
const
{
...
@@ -615,11 +618,33 @@ export function OverviewDashboard() {
...
@@ -615,11 +618,33 @@ export function OverviewDashboard() {
const
nextExpanded
=
!
setupGuideExpanded
const
nextExpanded
=
!
setupGuideExpanded
setManualSetupGuideExpanded
(
nextExpanded
)
setManualSetupGuideExpanded
(
nextExpanded
)
saveSetupGuideExpanded
(
nextExpanded
)
saveSetupGuideExpanded
(
nextExpanded
)
if
(
!
nextExpanded
&&
setupComplete
)
{
setupGuideToggleRef
.
current
?.
focus
()
}
}
}
return
(
return
(
<
SectionPageLayout
>
<
SectionPageLayout
.
Title
>
{
t
(
'Overview'
)
}
</
SectionPageLayout
.
Title
>
<
SectionPageLayout
.
Actions
>
{
setupStatusReady
&&
setupComplete
&&
(
<
Button
ref=
{
setupGuideToggleRef
}
variant=
'ghost'
size=
'sm'
className=
'text-muted-foreground hover:text-foreground h-auto min-h-7 max-w-[60vw] whitespace-normal'
aria
-
expanded=
{
setupGuideExpanded
}
aria
-
controls=
{
setupGuideId
}
onClick=
{
handleSetupGuideToggle
}
>
{
t
(
'Setup guide'
)
}
</
Button
>
)
}
</
SectionPageLayout
.
Actions
>
<
SectionPageLayout
.
Content
>
<
div
className=
'flex flex-col gap-4'
>
<
div
className=
'flex flex-col gap-4'
>
{
setupGuideExpanded
?
(
<
div
id=
{
setupGuideId
}
hidden=
{
!
setupGuideExpanded
}
>
{
setupGuideExpanded
&&
(
<
CardStaggerContainer
className=
'grid items-stretch gap-4 xl:grid-cols-[minmax(0,1fr)_22rem]'
>
<
CardStaggerContainer
className=
'grid items-stretch gap-4 xl:grid-cols-[minmax(0,1fr)_22rem]'
>
<
CardStaggerItem
className=
'bg-card h-full overflow-hidden rounded-2xl border shadow-xs'
>
<
CardStaggerItem
className=
'bg-card h-full overflow-hidden rounded-2xl border shadow-xs'
>
<
div
className=
'relative h-full overflow-hidden p-4 sm:p-5'
>
<
div
className=
'relative h-full overflow-hidden p-4 sm:p-5'
>
...
@@ -629,7 +654,10 @@ export function OverviewDashboard() {
...
@@ -629,7 +654,10 @@ export function OverviewDashboard() {
<
div
className=
'flex flex-wrap items-start justify-between gap-3'
>
<
div
className=
'flex flex-wrap items-start justify-between gap-3'
>
<
div
className=
'flex max-w-2xl flex-col gap-1'
>
<
div
className=
'flex max-w-2xl flex-col gap-1'
>
<
div
className=
'text-muted-foreground flex items-center gap-2 text-xs font-medium tracking-wider uppercase'
>
<
div
className=
'text-muted-foreground flex items-center gap-2 text-xs font-medium tracking-wider uppercase'
>
<
ListChecks
className=
'size-3.5'
aria
-
hidden=
'true'
/>
<
ListChecks
className=
'size-3.5'
aria
-
hidden=
'true'
/>
{
t
(
'Get started'
)
}
{
t
(
'Get started'
)
}
</
div
>
</
div
>
<
h3
className=
'text-xl font-semibold tracking-tight sm:text-2xl'
>
<
h3
className=
'text-xl font-semibold tracking-tight sm:text-2xl'
>
...
@@ -645,6 +673,8 @@ export function OverviewDashboard() {
...
@@ -645,6 +673,8 @@ export function OverviewDashboard() {
<
Button
<
Button
variant=
'outline'
variant=
'outline'
size=
'sm'
size=
'sm'
aria
-
expanded=
{
setupGuideExpanded
}
aria
-
controls=
{
setupGuideId
}
onClick=
{
handleSetupGuideToggle
}
onClick=
{
handleSetupGuideToggle
}
>
>
<
ChevronUp
data
-
icon=
'inline-start'
/>
<
ChevronUp
data
-
icon=
'inline-start'
/>
...
@@ -695,7 +725,9 @@ export function OverviewDashboard() {
...
@@ -695,7 +725,9 @@ export function OverviewDashboard() {
</
div
>
</
div
>
</
CardStaggerItem
>
</
CardStaggerItem
>
</
CardStaggerContainer
>
</
CardStaggerContainer
>
)
:
(
)
}
</
div
>
{
!
setupGuideExpanded
&&
!
setupComplete
&&
(
<
CardStaggerContainer
>
<
CardStaggerContainer
>
<
CardStaggerItem
className=
'bg-card overflow-hidden rounded-2xl border shadow-xs'
>
<
CardStaggerItem
className=
'bg-card overflow-hidden rounded-2xl border shadow-xs'
>
<
div
className=
'relative overflow-hidden px-4 py-3 sm:px-5'
>
<
div
className=
'relative overflow-hidden px-4 py-3 sm:px-5'
>
...
@@ -703,14 +735,15 @@ export function OverviewDashboard() {
...
@@ -703,14 +735,15 @@ export function OverviewDashboard() {
<
div
className=
'relative flex flex-wrap items-center justify-between gap-3'
>
<
div
className=
'relative flex flex-wrap items-center justify-between gap-3'
>
<
div
className=
'flex min-w-0 items-center gap-3'
>
<
div
className=
'flex min-w-0 items-center gap-3'
>
<
span
className=
'bg-background/70 flex size-9 shrink-0 items-center justify-center rounded-xl border shadow-xs'
>
<
span
className=
'bg-background/70 flex size-9 shrink-0 items-center justify-center rounded-xl border shadow-xs'
>
<
Check
className=
'text-success size-4'
aria
-
hidden=
'true'
/>
<
Check
className=
'text-success size-4'
aria
-
hidden=
'true'
/>
</
span
>
</
span
>
<
div
className=
'min-w-0'
>
<
div
className=
'min-w-0'
>
<
div
className=
'flex items-center gap-2'
>
<
div
className=
'flex items-center gap-2'
>
<
h3
className=
'truncate text-sm font-semibold'
>
<
h3
className=
'truncate text-sm font-semibold'
>
{
setupComplete
{
t
(
'Setup guide'
)
}
?
t
(
'Setup guide complete'
)
:
t
(
'Setup guide'
)
}
</
h3
>
</
h3
>
<
span
className=
'text-muted-foreground bg-background/60 rounded-md border px-2 py-0.5 text-xs'
>
<
span
className=
'text-muted-foreground bg-background/60 rounded-md border px-2 py-0.5 text-xs'
>
{
t
(
'
Setup
progress
:
{{
completed
}}
/{{total}}', {
{
t
(
'
Setup
progress
:
{{
completed
}}
/{{total}}', {
...
@@ -720,23 +753,24 @@ export function OverviewDashboard() {
...
@@ -720,23 +753,24 @@ export function OverviewDashboard() {
</
span
>
</
span
>
</
div
>
</
div
>
<
p
className=
'text-muted-foreground line-clamp-1 text-xs'
>
<
p
className=
'text-muted-foreground line-clamp-1 text-xs'
>
{
setupComplete
{
t
(
'Setup guide is collapsed. Expand it anytime.'
)
}
?
t
(
'Your setup guide is collapsed so usage stays in focus.'
)
:
t
(
'Setup guide is collapsed. Expand it anytime.'
)
}
</
p
>
</
p
>
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
'flex flex-wrap items-center gap-2'
>
<
div
className=
'flex flex-wrap items-center gap-2'
>
{
visibleQuickActions
.
map
((
action
)
=>
(
{
visibleQuickActions
.
map
((
action
)
=>
(
<
CompactQuickAction
key=
{
action
.
title
}
action=
{
action
}
/>
<
CompactQuickAction
key=
{
action
.
title
}
action=
{
action
}
/>
))
}
))
}
<
Button
<
Button
variant=
'outline'
variant=
'outline'
size=
'sm'
size=
'sm'
className=
'bg-background/70 h-8 min-w-28'
className=
'bg-background/70 h-8 min-w-28'
aria
-
expanded=
{
setupGuideExpanded
}
aria
-
controls=
{
setupGuideId
}
onClick=
{
handleSetupGuideToggle
}
onClick=
{
handleSetupGuideToggle
}
>
>
<
ChevronDown
data
-
icon=
'inline-start'
/>
<
ChevronDown
data
-
icon=
'inline-start'
/>
...
@@ -764,7 +798,9 @@ export function OverviewDashboard() {
...
@@ -764,7 +798,9 @@ export function OverviewDashboard() {
<
div
<
div
className=
{
cn
(
className=
{
cn
(
'grid min-w-0 grid-cols-1 gap-4'
,
'grid min-w-0 grid-cols-1 gap-4'
,
(
showApiInfoPanel
||
showAnnouncementsPanel
||
showFAQPanel
)
&&
(
showApiInfoPanel
||
showAnnouncementsPanel
||
showFAQPanel
)
&&
'lg:grid-cols-2'
'lg:grid-cols-2'
)
}
)
}
>
>
...
@@ -798,5 +834,7 @@ export function OverviewDashboard() {
...
@@ -798,5 +834,7 @@ export function OverviewDashboard() {
</
CardStaggerContainer
>
</
CardStaggerContainer
>
)
}
)
}
</
div
>
</
div
>
</
SectionPageLayout
.
Content
>
</
SectionPageLayout
>
)
)
}
}
web/src/features/dashboard/index.tsx
View file @
521cebf5
...
@@ -317,12 +317,15 @@ export function Dashboard() {
...
@@ -317,12 +317,15 @@ export function Dashboard() {
)
:
null
)
:
null
const
sectionActions
=
modelActions
??
flowActions
const
sectionActions
=
modelActions
??
flowActions
if
(
activeSection
===
'overview'
)
{
return
<
OverviewDashboard
/>
}
return
(
return
(
<
SectionPageLayout
>
<
SectionPageLayout
>
<
SectionPageLayout
.
Title
>
{
t
(
meta
.
titleKey
)
}
</
SectionPageLayout
.
Title
>
<
SectionPageLayout
.
Title
>
{
t
(
meta
.
titleKey
)
}
</
SectionPageLayout
.
Title
>
<
SectionPageLayout
.
Content
>
<
SectionPageLayout
.
Content
>
<
div
className=
'space-y-3 sm:space-y-4'
>
<
div
className=
'space-y-3 sm:space-y-4'
>
{
activeSection
!==
'overview'
&&
(
<
div
className=
'flex flex-wrap items-center justify-between gap-1.5 sm:gap-2'
>
<
div
className=
'flex flex-wrap items-center justify-between gap-1.5 sm:gap-2'
>
{
showSectionTabs
?
(
{
showSectionTabs
?
(
<
Tabs
value=
{
activeSection
}
onValueChange=
{
handleSectionChange
}
>
<
Tabs
value=
{
activeSection
}
onValueChange=
{
handleSectionChange
}
>
...
@@ -343,8 +346,6 @@ export function Dashboard() {
...
@@ -343,8 +346,6 @@ export function Dashboard() {
</
div
>
</
div
>
)
}
)
}
</
div
>
</
div
>
)
}
{
activeSection
===
'overview'
&&
<
OverviewDashboard
/>
}
{
activeSection
===
'models'
&&
(
{
activeSection
===
'models'
&&
(
<>
<>
<
FadeIn
>
<
FadeIn
>
...
...
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