Skip to content

Commit 7d2a7a3

Browse files
feat(admin-ui): add debounced search, sidebar sections with icons, and UI polish
Replace form-submit search with debounced search across all list pages (Users, Roles, Policies, Data Sources, Attributes). Add keepPreviousData to all paginated queries to prevent layout flash. Restructure sidebar navigation into Access Control / Data / Activity sections with Heroicons. Add attribute definition search endpoint (backend + frontend), entity type column to attributes table, SUPPORTED_ENTITY_TYPES constant, and "Report an issue" link in sidebar footer. Reduce audit timeline page size to 5.
1 parent 18e6847 commit 7d2a7a3

16 files changed

Lines changed: 274 additions & 269 deletions

admin-ui/src/api/attributeDefinitions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { PaginatedResponse } from '../types/user'
88

99
export async function listAttributeDefinitions(params?: {
1010
entity_type?: string
11+
search?: string
1112
page?: number
1213
page_size?: number
1314
}): Promise<PaginatedResponse<AttributeDefinition>> {

admin-ui/src/components/AttributeDefinitionForm.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { type FormEvent, useState, useEffect } from 'react'
2-
import type {
3-
ValueType,
4-
EntityType,
5-
AttributeDefinition,
2+
import {
3+
SUPPORTED_ENTITY_TYPES,
4+
type ValueType,
5+
type EntityType,
6+
type AttributeDefinition,
67
} from '../types/attributeDefinition'
78

89
export interface AttributeDefinitionFormValues {
@@ -26,7 +27,7 @@ interface Props {
2627
}
2728

2829
const VALUE_TYPES: ValueType[] = ['string', 'integer', 'boolean', 'list']
29-
const ENTITY_TYPES: EntityType[] = ['user', 'table', 'column']
30+
const ENTITY_TYPES = SUPPORTED_ENTITY_TYPES
3031

3132
const inputCls =
3233
'w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'

admin-ui/src/components/AuditTimeline.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function AuditTimeline({ resourceType, resourceId }: AuditTimelineProps)
1818
resource_type: resourceType,
1919
resource_id: resourceId,
2020
page,
21-
page_size: 20,
21+
page_size: 5,
2222
}),
2323
})
2424

@@ -74,7 +74,7 @@ export function AuditTimeline({ resourceType, resourceId }: AuditTimelineProps)
7474
</div>
7575

7676
{totalPages > 1 && (
77-
<div className="flex items-center gap-2 mt-4 justify-end">
77+
<div className="flex items-center gap-2 mt-4">
7878
<button
7979
onClick={() => setPage((p) => Math.max(1, p - 1))}
8080
disabled={page === 1}

admin-ui/src/components/Layout.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('Layout', () => {
4444

4545
it('shows the logged-in username', () => {
4646
renderWithProviders(<WrappedLayout />, { authenticated: true, routerEntries: ['/'] })
47-
expect(screen.getByText('admin')).toBeInTheDocument()
47+
expect(screen.getByText('@admin')).toBeInTheDocument()
4848
})
4949

5050
it('sign out button clears auth and navigates to /login', async () => {

admin-ui/src/components/Layout.tsx

Lines changed: 145 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -21,105 +21,155 @@ export function Layout() {
2121
<p className="text-xs text-gray-400 mt-0.5">Admin Console</p>
2222
</div>
2323

24-
<nav className="flex-1 px-3 py-4 space-y-1">
25-
<NavLink
26-
to="/"
27-
end
28-
className={({ isActive }) =>
29-
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
30-
isActive
31-
? 'bg-blue-600 text-white'
32-
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
33-
}`
34-
}
35-
>
36-
Users
37-
</NavLink>
38-
<NavLink
39-
to="/datasources"
40-
className={({ isActive }) =>
41-
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
42-
isActive
43-
? 'bg-blue-600 text-white'
44-
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
45-
}`
46-
}
47-
>
48-
Data Sources
49-
</NavLink>
50-
<NavLink
51-
to="/roles"
52-
className={({ isActive }) =>
53-
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
54-
isActive
55-
? 'bg-blue-600 text-white'
56-
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
57-
}`
58-
}
59-
>
60-
Roles
61-
</NavLink>
62-
<NavLink
63-
to="/attributes"
64-
className={({ isActive }) =>
65-
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
66-
isActive
67-
? 'bg-blue-600 text-white'
68-
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
69-
}`
70-
}
71-
>
72-
Attributes
73-
</NavLink>
74-
<NavLink
75-
to="/policies"
76-
className={({ isActive }) =>
77-
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
78-
isActive
79-
? 'bg-blue-600 text-white'
80-
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
81-
}`
82-
}
83-
>
84-
Policies
85-
</NavLink>
86-
<NavLink
87-
to="/audit"
88-
className={({ isActive }) =>
89-
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
90-
isActive
91-
? 'bg-blue-600 text-white'
92-
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
93-
}`
94-
}
95-
>
96-
Query Audit
97-
</NavLink>
98-
<NavLink
99-
to="/admin-audit"
100-
className={({ isActive }) =>
101-
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
102-
isActive
103-
? 'bg-blue-600 text-white'
104-
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
105-
}`
106-
}
107-
>
108-
Admin Audit
109-
</NavLink>
24+
<nav className="flex-1 px-3 py-4 space-y-4">
25+
{/* Access Control */}
26+
<div className="space-y-1">
27+
<p className="px-3 text-[10px] font-semibold text-gray-500 uppercase tracking-wider">Access Control</p>
28+
<NavLink
29+
to="/"
30+
end
31+
className={({ isActive }) =>
32+
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
33+
isActive
34+
? 'bg-blue-600 text-white'
35+
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
36+
}`
37+
}
38+
>
39+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
40+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
41+
</svg>
42+
Users
43+
</NavLink>
44+
<NavLink
45+
to="/roles"
46+
className={({ isActive }) =>
47+
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
48+
isActive
49+
? 'bg-blue-600 text-white'
50+
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
51+
}`
52+
}
53+
>
54+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
55+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
56+
</svg>
57+
Roles
58+
</NavLink>
59+
<NavLink
60+
to="/attributes"
61+
className={({ isActive }) =>
62+
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
63+
isActive
64+
? 'bg-blue-600 text-white'
65+
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
66+
}`
67+
}
68+
>
69+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
70+
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z" />
71+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 6h.008v.008H6V6Z" />
72+
</svg>
73+
Attributes
74+
</NavLink>
75+
<NavLink
76+
to="/policies"
77+
className={({ isActive }) =>
78+
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
79+
isActive
80+
? 'bg-blue-600 text-white'
81+
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
82+
}`
83+
}
84+
>
85+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
86+
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
87+
</svg>
88+
Policies
89+
</NavLink>
90+
</div>
91+
92+
{/* Data */}
93+
<div className="space-y-1">
94+
<p className="px-3 text-[10px] font-semibold text-gray-500 uppercase tracking-wider">Data</p>
95+
<NavLink
96+
to="/datasources"
97+
className={({ isActive }) =>
98+
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
99+
isActive
100+
? 'bg-blue-600 text-white'
101+
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
102+
}`
103+
}
104+
>
105+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
106+
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
107+
</svg>
108+
Data Sources
109+
</NavLink>
110+
</div>
111+
112+
{/* Activity */}
113+
<div className="space-y-1">
114+
<p className="px-3 text-[10px] font-semibold text-gray-500 uppercase tracking-wider">Activity</p>
115+
<NavLink
116+
to="/audit"
117+
className={({ isActive }) =>
118+
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
119+
isActive
120+
? 'bg-blue-600 text-white'
121+
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
122+
}`
123+
}
124+
>
125+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
126+
<path strokeLinecap="round" strokeLinejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
127+
</svg>
128+
Query Audit
129+
</NavLink>
130+
<NavLink
131+
to="/admin-audit"
132+
className={({ isActive }) =>
133+
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
134+
isActive
135+
? 'bg-blue-600 text-white'
136+
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
137+
}`
138+
}
139+
>
140+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
141+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15a2.25 2.25 0 0 1 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25ZM6.75 12h.008v.008H6.75V12Zm0 3h.008v.008H6.75V15Zm0 3h.008v.008H6.75V18Z" />
142+
</svg>
143+
Admin Audit
144+
</NavLink>
145+
</div>
110146
</nav>
111147

112-
<div className="px-4 py-4 border-t border-gray-700">
113-
<p className="text-xs text-gray-400 truncate">{user?.username}</p>
148+
<div className="px-4 py-4 border-t border-gray-700 space-y-2">
149+
<p className="text-xs text-gray-400 truncate">@{user?.username}</p>
114150
{version && (
115-
<p className="text-xs text-gray-500 mt-1">v{version.current} ({version.commit})</p>
151+
<p className="text-xs text-gray-500">v{version.current} ({version.commit})</p>
116152
)}
117-
<button
118-
onClick={handleSignOut}
119-
className="mt-2 text-xs text-gray-400 hover:text-white transition-colors"
120-
>
121-
Sign out
122-
</button>
153+
<div className="flex items-center gap-3">
154+
<button
155+
onClick={handleSignOut}
156+
className="text-xs text-gray-400 hover:text-white transition-colors"
157+
>
158+
Sign out
159+
</button>
160+
<span className="text-gray-600">·</span>
161+
<a
162+
href="https://github.com/getbetweenrows/betweenrows/issues"
163+
target="_blank"
164+
rel="noopener noreferrer"
165+
className="inline-flex items-center gap-1 text-xs text-gray-400 hover:text-white transition-colors"
166+
>
167+
Report an issue
168+
<svg className="h-2.5 w-2.5" viewBox="0 0 20 20" fill="currentColor">
169+
<path fillRule="evenodd" d="M4.25 5.5a.75.75 0 00-.75.75v8.5c0 .414.336.75.75.75h8.5a.75.75 0 00.75-.75v-4a.75.75 0 011.5 0v4A2.25 2.25 0 0112.75 17h-8.5A2.25 2.25 0 012 14.75v-8.5A2.25 2.25 0 014.25 4h5a.75.75 0 010 1.5h-5zm7.25-.75a.75.75 0 01.75-.75h3.5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0V6.31l-5.47 5.47a.75.75 0 01-1.06-1.06l5.47-5.47H12.25a.75.75 0 01-.75-.75z" clipRule="evenodd" />
170+
</svg>
171+
</a>
172+
</div>
123173
</div>
124174
</aside>
125175

admin-ui/src/pages/AdminAuditPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Fragment, useState, useMemo } from 'react'
2-
import { useQuery } from '@tanstack/react-query'
2+
import { keepPreviousData, useQuery } from '@tanstack/react-query'
33
import { listAdminAuditLogs } from '../api/adminAudit'
44
import { actionBadgeClass } from '../utils/auditBadge'
55
import { EntitySelect } from '../components/EntitySelect'
@@ -70,6 +70,7 @@ export function AdminAuditPage() {
7070
page_size: 20,
7171
...appliedFilters,
7272
}),
73+
placeholderData: keepPreviousData,
7374
})
7475

7576
function handleFilter(e: React.FormEvent) {

0 commit comments

Comments
 (0)