Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
F
frontend-data-crawler-be
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
BangNSK
frontend-data-crawler-be
Commits
37bafeba
You need to sign in or sign up before continuing.
Commit
37bafeba
authored
Aug 04, 2026
by
BangNSK
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add webhook management interface
parent
a5729e5c
Changes
6
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
785 additions
and
6 deletions
+785
-6
App.tsx
src/App.tsx
+2
-0
PageTitle.tsx
src/components/PageTitle.tsx
+1
-0
DashboardLayout.tsx
src/layouts/DashboardLayout.tsx
+42
-6
Webhooks.tsx
src/pages/Webhooks.tsx
+661
-0
api.ts
src/services/api.ts
+62
-0
feedback-regression.test.mjs
tests/feedback-regression.test.mjs
+17
-0
No files found.
src/App.tsx
View file @
37bafeba
...
...
@@ -11,6 +11,7 @@ import { Users } from './pages/Users';
import
{
Logs
}
from
'./pages/Logs'
;
import
{
Profile
}
from
'./pages/Profile'
;
import
{
ApiKeys
}
from
'./pages/ApiKeys'
;
import
{
Webhooks
}
from
'./pages/Webhooks'
;
import
{
ForgotPassword
}
from
'./pages/ForgotPassword'
;
import
{
ResetPassword
}
from
'./pages/ResetPassword'
;
import
{
VerifyEmail
}
from
'./pages/VerifyEmail'
;
...
...
@@ -61,6 +62,7 @@ export default function App() {
{
/* Profile */
}
<
Route
path=
"profile"
element=
{
<
Profile
/>
}
/>
<
Route
path=
"api-keys"
element=
{
<
ApiKeys
/>
}
/>
<
Route
path=
"webhooks"
element=
{
<
Webhooks
/>
}
/>
{
/* Admin Only Routes */
}
<
Route
...
...
src/components/PageTitle.tsx
View file @
37bafeba
...
...
@@ -10,6 +10,7 @@ const TITLES: Record<string, string> = {
'/verify-email'
:
'Xác minh email | Data Crawler'
,
'/profile'
:
'Hồ sơ cá nhân | Data Crawler'
,
'/api-keys'
:
'Quản lý API Key | Data Crawler'
,
'/webhooks'
:
'Quản lý Webhook | Data Crawler'
,
'/users'
:
'Quản lý người dùng | Data Crawler'
,
'/logs'
:
'Nhật ký hệ thống | Data Crawler'
,
};
...
...
src/layouts/DashboardLayout.tsx
View file @
37bafeba
import
React
from
'react'
;
import
{
Link
,
useNavigate
,
useLocation
,
Outlet
}
from
'react-router-dom'
;
import
{
useAuth
}
from
'../context/auth'
;
import
{
Database
,
Users
,
History
,
User
,
LogOut
,
Compass
,
KeyRound
}
from
'lucide-react'
;
import
{
Database
,
Users
,
History
,
User
,
LogOut
,
Compass
,
KeyRound
,
MoreHorizontal
,
Webhook
}
from
'lucide-react'
;
export
const
DashboardLayout
:
React
.
FC
=
()
=>
{
const
{
user
,
logout
,
isAdmin
}
=
useAuth
();
...
...
@@ -26,6 +26,12 @@ export const DashboardLayout: React.FC = () => {
icon
:
<
KeyRound
className=
"h-5 w-5"
/>,
allowed
:
true
,
},
{
name
:
'Quản lý Webhook'
,
path
:
'/webhooks'
,
icon
:
<
Webhook
className=
"h-5 w-5"
/>,
allowed
:
true
,
},
{
name
:
'Quản lý Users'
,
path
:
'/users'
,
...
...
@@ -51,6 +57,10 @@ export const DashboardLayout: React.FC = () => {
?
location
.
pathname
===
'/'
||
location
.
pathname
.
startsWith
(
'/jobs/'
)
:
location
.
pathname
===
path
;
const
visibleNavItems
=
navItems
.
filter
((
item
)
=>
item
.
allowed
);
const
mobilePrimaryItems
=
visibleNavItems
.
length
>
5
?
visibleNavItems
.
slice
(
0
,
4
)
:
visibleNavItems
;
const
mobileOverflowItems
=
visibleNavItems
.
length
>
5
?
visibleNavItems
.
slice
(
4
)
:
[];
return
(
<
div
className=
"flex h-screen w-screen overflow-hidden bg-slate-50 text-slate-800"
>
{
/* Sidebar */
}
...
...
@@ -68,8 +78,7 @@ export const DashboardLayout: React.FC = () => {
{
/* Navigation Links */
}
<
nav
className=
"flex-1 px-2 space-y-1 bg-white"
>
{
navItems
.
filter
((
item
)
=>
item
.
allowed
)
{
visibleNavItems
.
map
((
item
)
=>
{
const
isActive
=
isPathActive
(
item
.
path
);
return
(
...
...
@@ -142,9 +151,7 @@ export const DashboardLayout: React.FC = () => {
</
header
>
<
nav
className=
"fixed inset-x-0 bottom-0 z-40 grid grid-flow-col auto-cols-fr border-t border-slate-200 bg-white/95 px-2 pb-[max(0.5rem,env(safe-area-inset-bottom))] pt-2 shadow-lg backdrop-blur md:hidden"
aria
-
label=
"Điều hướng chính"
>
{
navItems
.
filter
((
item
)
=>
item
.
allowed
)
.
map
((
item
)
=>
{
{
mobilePrimaryItems
.
map
((
item
)
=>
{
const
isActive
=
isPathActive
(
item
.
path
);
return
(
<
Link
...
...
@@ -159,6 +166,35 @@ export const DashboardLayout: React.FC = () => {
</
Link
>
);
})
}
{
mobileOverflowItems
.
length
>
0
&&
(
<
details
className=
"group relative"
>
<
summary
className=
{
`flex min-w-0 cursor-pointer list-none flex-col items-center gap-1 rounded-lg px-1 py-1.5 text-[10px] font-medium ${
mobileOverflowItems.some((item) => isPathActive(item.path))
? 'bg-indigo-50 text-indigo-700'
: 'text-slate-500'
}`
}
>
<
MoreHorizontal
className=
"h-5 w-5"
/>
<
span
>
Thêm
</
span
>
</
summary
>
<
div
className=
"fixed bottom-20 right-2 z-50 w-56 overflow-hidden rounded-xl border border-slate-200 bg-white p-1.5 shadow-xl"
>
{
mobileOverflowItems
.
map
((
item
)
=>
(
<
Link
key=
{
item
.
name
}
to=
{
item
.
path
}
onClick=
{
(
event
)
=>
event
.
currentTarget
.
closest
(
'details'
)?.
removeAttribute
(
'open'
)
}
className=
{
`flex min-h-11 items-center rounded-lg px-3 text-sm font-medium ${
isPathActive(item.path)
? 'bg-indigo-50 text-indigo-700'
: 'text-slate-600 hover:bg-slate-50'
}`
}
>
<
span
className=
"mr-3"
>
{
item
.
icon
}
</
span
>
{
item
.
name
}
</
Link
>
))
}
</
div
>
</
details
>
)
}
</
nav
>
{
/* Page Content */
}
...
...
src/pages/Webhooks.tsx
0 → 100644
View file @
37bafeba
This diff is collapsed.
Click to expand it.
src/services/api.ts
View file @
37bafeba
...
...
@@ -16,6 +16,38 @@ export interface CreatedApiKey extends ApiKeyRecord {
rawKey
:
string
;
}
export
type
WebhookEvent
=
'job.completed'
|
'job.failed'
;
export
type
WebhookDeliveryStatus
=
'PENDING'
|
'SUCCESS'
|
'FAILED'
;
export
interface
WebhookConfig
{
id
:
string
;
userId
:
string
;
url
:
string
;
isActive
:
boolean
;
events
:
WebhookEvent
[];
createdAt
:
string
;
updatedAt
:
string
;
}
export
interface
WebhookDelivery
{
id
:
string
;
webhookConfigId
:
string
;
crawlJobId
:
string
;
event
:
WebhookEvent
;
payload
:
unknown
;
status
:
WebhookDeliveryStatus
;
statusCode
:
number
|
null
;
attempt
:
number
;
responseBody
:
string
|
null
;
errorMessage
:
string
|
null
;
deliveredAt
:
string
|
null
;
createdAt
:
string
;
updatedAt
:
string
;
webhookConfig
:
{
url
:
string
;
};
}
interface
ApiResponse
<
T
>
{
success
:
boolean
;
data
:
T
;
...
...
@@ -133,6 +165,36 @@ export const apiKeysApi = {
},
};
export
const
webhooksApi
=
{
async
listConfigs
():
Promise
<
WebhookConfig
[]
>
{
const
response
=
await
api
.
get
<
ApiResponse
<
WebhookConfig
[]
>>
(
'/webhooks/configs'
);
return
response
.
data
.
data
;
},
async
createConfig
(
input
:
{
url
:
string
;
secret
:
string
;
events
:
WebhookEvent
[];
}):
Promise
<
WebhookConfig
>
{
const
response
=
await
api
.
post
<
ApiResponse
<
WebhookConfig
>>
(
'/webhooks/configs'
,
input
);
return
response
.
data
.
data
;
},
async
deleteConfig
(
id
:
string
):
Promise
<
void
>
{
await
api
.
delete
(
`/webhooks/configs/
${
id
}
`
);
},
async
listDeliveries
(
filters
:
{
jobId
?:
string
;
status
?:
WebhookDeliveryStatus
;
}
=
{}):
Promise
<
WebhookDelivery
[]
>
{
const
response
=
await
api
.
get
<
ApiResponse
<
WebhookDelivery
[]
>>
(
'/webhooks/deliveries'
,
{
params
:
filters
,
});
return
response
.
data
.
data
;
},
};
function
notifyAuthExpired
()
{
localStorage
.
removeItem
(
'accessToken'
);
localStorage
.
removeItem
(
'refreshToken'
);
...
...
tests/feedback-regression.test.mjs
View file @
37bafeba
...
...
@@ -59,3 +59,20 @@ test('profile rejects no-op submissions instead of reporting success', () => {
assert
.
match
(
profile
,
/Bạn chưa thay đổi thông tin nào/
);
assert
.
match
(
profile
,
/profileChanged/
);
});
test
(
'webhook management is routed and integrates the supported backend endpoints'
,
()
=>
{
const
app
=
read
(
'App.tsx'
);
const
layout
=
read
(
'layouts/DashboardLayout.tsx'
);
const
pageTitle
=
read
(
'components/PageTitle.tsx'
);
const
api
=
read
(
'services/api.ts'
);
const
webhooks
=
read
(
'pages/Webhooks.tsx'
);
assert
.
match
(
app
,
/path="webhooks"/
);
assert
.
match
(
layout
,
/Quản lý Webhook/
);
assert
.
match
(
pageTitle
,
/Quản lý Webhook
\|
Data Crawler/
);
assert
.
match
(
api
,
/
\/
webhooks
\/
configs/
);
assert
.
match
(
api
,
/
\/
webhooks
\/
deliveries/
);
assert
.
match
(
webhooks
,
/job
\.
completed/
);
assert
.
match
(
webhooks
,
/job
\.
failed/
);
assert
.
match
(
webhooks
,
/X-Webhook-Signature/
);
});
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