Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
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
Vũ Hoàng Anh
canifa_note
Commits
0026f9e5
Commit
0026f9e5
authored
Nov 28, 2023
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore(frontend): add webhooks section
parent
f8f73d11
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
288 additions
and
4 deletions
+288
-4
memo.go
api/v1/memo.go
+4
-4
CreateWebhookDialog.tsx
web/src/components/CreateWebhookDialog.tsx
+150
-0
PreferencesSection.tsx
web/src/components/Settings/PreferencesSection.tsx
+5
-0
WebhookSection.tsx
web/src/components/Settings/WebhookSection.tsx
+126
-0
grpcweb.ts
web/src/grpcweb.ts
+3
-0
No files found.
api/v1/memo.go
View file @
0026f9e5
...
...
@@ -426,7 +426,7 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
}
// Try to dispatch webhook when memo is created.
if
err
:=
s
.
DispatchMemoCreatedWebhook
(
ctx
,
memoResponse
);
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to dispatch memo created webhook"
)
.
SetInternal
(
err
)
log
.
Warn
(
"Failed to dispatch memo created webhook"
,
zap
.
Error
(
err
)
)
}
metric
.
Enqueue
(
"memo create"
)
...
...
@@ -801,9 +801,9 @@ func (s *APIV1Service) UpdateMemo(c echo.Context) error {
if
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to compose memo response"
)
.
SetInternal
(
err
)
}
// Try to dispatch webhook when memo is
cre
ated.
if
err
:=
s
.
DispatchMemo
Cre
atedWebhook
(
ctx
,
memoResponse
);
err
!=
nil
{
return
echo
.
NewHTTPError
(
http
.
StatusInternalServerError
,
"Failed to dispatch memo created webhook"
)
.
SetInternal
(
err
)
// Try to dispatch webhook when memo is
upd
ated.
if
err
:=
s
.
DispatchMemo
Upd
atedWebhook
(
ctx
,
memoResponse
);
err
!=
nil
{
log
.
Warn
(
"Failed to dispatch memo updated webhook"
,
zap
.
Error
(
err
)
)
}
return
c
.
JSON
(
http
.
StatusOK
,
memoResponse
)
...
...
web/src/components/CreateWebhookDialog.tsx
0 → 100644
View file @
0026f9e5
import
{
Button
,
Input
}
from
"@mui/joy"
;
import
React
,
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
webhookServiceClient
}
from
"@/grpcweb"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
generateDialog
}
from
"./Dialog"
;
import
Icon
from
"./Icon"
;
interface
Props
extends
DialogProps
{
webhookId
?:
number
;
onConfirm
:
()
=>
void
;
}
interface
State
{
name
:
string
;
url
:
string
;
}
const
CreateWebhookDialog
:
React
.
FC
<
Props
>
=
(
props
:
Props
)
=>
{
const
{
webhookId
,
destroy
,
onConfirm
}
=
props
;
const
t
=
useTranslate
();
const
[
state
,
setState
]
=
useState
({
name
:
""
,
url
:
""
,
});
const
requestState
=
useLoading
(
false
);
const
isCreating
=
webhookId
===
undefined
;
useEffect
(()
=>
{
if
(
webhookId
)
{
webhookServiceClient
.
getWebhook
({
id
:
webhookId
,
})
.
then
(({
webhook
})
=>
{
if
(
!
webhook
)
{
return
;
}
setState
({
name
:
webhook
.
name
,
url
:
webhook
.
url
,
});
});
}
},
[]);
const
setPartialState
=
(
partialState
:
Partial
<
State
>
)
=>
{
setState
({
...
state
,
...
partialState
,
});
};
const
handleTitleInputChange
=
(
e
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
setPartialState
({
name
:
e
.
target
.
value
,
});
};
const
handleUrlInputChange
=
(
e
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
setPartialState
({
url
:
e
.
target
.
value
,
});
};
const
handleSaveBtnClick
=
async
()
=>
{
if
(
!
state
.
name
||
!
state
.
url
)
{
toast
.
error
(
"Please fill all required fields"
);
return
;
}
try
{
if
(
isCreating
)
{
await
webhookServiceClient
.
createWebhook
({
name
:
state
.
name
,
url
:
state
.
url
,
});
}
else
{
await
webhookServiceClient
.
updateWebhook
({
webhook
:
{
id
:
webhookId
,
name
:
state
.
name
,
url
:
state
.
url
,
},
updateMask
:
[
"name"
,
"url"
],
});
}
onConfirm
();
destroy
();
}
catch
(
error
:
any
)
{
console
.
error
(
error
);
toast
.
error
(
error
.
details
);
}
};
return
(
<>
<
div
className=
"dialog-header-container"
>
<
p
className=
"title-text"
>
{
isCreating
?
"Create webhook"
:
"Edit webhook"
}
</
p
>
<
button
className=
"btn close-btn"
onClick=
{
()
=>
destroy
()
}
>
<
Icon
.
X
/>
</
button
>
</
div
>
<
div
className=
"dialog-content-container !w-80"
>
<
div
className=
"w-full flex flex-col justify-start items-start mb-3"
>
<
span
className=
"mb-2"
>
Title
<
span
className=
"text-red-600"
>
*
</
span
>
</
span
>
<
div
className=
"relative w-full"
>
<
Input
className=
"w-full"
type=
"text"
placeholder=
""
value=
{
state
.
name
}
onChange=
{
handleTitleInputChange
}
/>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-col justify-start items-start mb-3"
>
<
span
className=
"mb-2"
>
Url
<
span
className=
"text-red-600"
>
*
</
span
>
</
span
>
<
div
className=
"relative w-full"
>
<
Input
className=
"w-full"
type=
"text"
placeholder=
"Callback endpoint"
value=
{
state
.
url
}
onChange=
{
handleUrlInputChange
}
/>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center mt-4 space-x-2"
>
<
Button
color=
"neutral"
variant=
"plain"
disabled=
{
requestState
.
isLoading
}
loading=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
disabled=
{
requestState
.
isLoading
}
loading=
{
requestState
.
isLoading
}
onClick=
{
handleSaveBtnClick
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
</
div
>
</>
);
};
function
showCreateWebhookDialog
(
onConfirm
:
()
=>
void
)
{
generateDialog
(
{
className
:
"create-webhook-dialog"
,
dialogName
:
"create-webhook-dialog"
,
},
CreateWebhookDialog
,
{
onConfirm
,
}
);
}
export
default
showCreateWebhookDialog
;
web/src/components/Settings/PreferencesSection.tsx
View file @
0026f9e5
...
...
@@ -8,6 +8,7 @@ import AppearanceSelect from "../AppearanceSelect";
import
LearnMore
from
"../LearnMore"
;
import
LocaleSelect
from
"../LocaleSelect"
;
import
VisibilityIcon
from
"../VisibilityIcon"
;
import
WebhookSection
from
"./WebhookSection"
;
import
"@/less/settings/preferences-section.less"
;
const
PreferencesSection
=
()
=>
{
...
...
@@ -106,6 +107,10 @@ const PreferencesSection = () => {
onChange=
{
(
event
)
=>
handleTelegramUserIdChanged
(
event
.
target
.
value
)
}
placeholder=
{
t
(
"setting.preference-section.telegram-user-id-placeholder"
)
}
/>
<
Divider
className=
"!mt-3 !my-4"
/>
<
WebhookSection
/>
</
div
>
);
};
...
...
web/src/components/Settings/WebhookSection.tsx
0 → 100644
View file @
0026f9e5
import
{
Button
,
IconButton
}
from
"@mui/joy"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
webhookServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
Webhook
}
from
"@/types/proto/api/v2/webhook_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
showCreateWebhookDialog
from
"../CreateWebhookDialog"
;
import
{
showCommonDialog
}
from
"../Dialog/CommonDialog"
;
import
Icon
from
"../Icon"
;
import
LearnMore
from
"../LearnMore"
;
const
listWebhooks
=
async
(
userId
:
number
)
=>
{
const
{
webhooks
}
=
await
webhookServiceClient
.
listWebhooks
({
creatorId
:
userId
,
});
return
webhooks
;
};
const
WebhookSection
=
()
=>
{
const
t
=
useTranslate
();
const
currentUser
=
useCurrentUser
();
const
[
webhooks
,
setWebhooks
]
=
useState
<
Webhook
[]
>
([]);
useEffect
(()
=>
{
listWebhooks
(
currentUser
.
id
).
then
((
webhooks
)
=>
{
setWebhooks
(
webhooks
);
});
},
[]);
const
handleCreateAccessTokenDialogConfirm
=
async
()
=>
{
const
webhooks
=
await
listWebhooks
(
currentUser
.
id
);
setWebhooks
(
webhooks
);
};
const
handleDeleteWebhook
=
async
(
webhook
:
Webhook
)
=>
{
showCommonDialog
({
title
:
"Delete Webhook"
,
content
:
`Are you sure to delete webhook \`
${
webhook
.
name
}
\`? You cannot undo this action.`
,
style
:
"danger"
,
dialogName
:
"delete-webhook-dialog"
,
onConfirm
:
async
()
=>
{
await
webhookServiceClient
.
deleteWebhook
({
id
:
webhook
.
id
});
setWebhooks
(
webhooks
.
filter
((
item
)
=>
item
.
id
!==
webhook
.
id
));
},
});
};
return
(
<>
<
div
className=
"w-full flex flex-col justify-start items-start space-y-4"
>
<
div
className=
"w-full"
>
<
div
className=
"flex justify-between items-center"
>
<
div
className=
"flex-auto space-y-1"
>
<
p
className=
"flex flex-row justify-start items-center font-medium text-gray-700 dark:text-gray-300"
>
Webhooks
<
LearnMore
className=
"ml-2"
url=
"https://usememos.com/docs/advanced-settings/webhook"
/>
</
p
>
</
div
>
<
div
>
<
Button
variant=
"outlined"
color=
"neutral"
onClick=
{
()
=>
{
showCreateWebhookDialog
(
handleCreateAccessTokenDialogConfirm
);
}
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
</
div
>
<
div
className=
"mt-2 flow-root"
>
<
div
className=
"overflow-x-auto"
>
<
div
className=
"inline-block min-w-full border rounded-lg align-middle"
>
<
table
className=
"min-w-full divide-y divide-gray-300 dark:divide-gray-400"
>
<
thead
>
<
tr
>
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-400"
>
Name
</
th
>
<
th
scope=
"col"
className=
"px-3 py-2 text-left text-sm font-semibold text-gray-900 dark:text-gray-400"
>
Url
</
th
>
<
th
scope=
"col"
className=
"relative px-3 py-2 pr-4"
>
<
span
className=
"sr-only"
>
{
t
(
"common.delete"
)
}
</
span
>
</
th
>
</
tr
>
</
thead
>
<
tbody
className=
"divide-y divide-gray-200 dark:divide-gray-500"
>
{
webhooks
.
map
((
webhook
)
=>
(
<
tr
key=
{
webhook
.
id
}
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-gray-900 dark:text-gray-400"
>
{
webhook
.
name
}
</
td
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-gray-900 dark:text-gray-400"
>
{
webhook
.
url
}
</
td
>
<
td
className=
"relative whitespace-nowrap px-3 py-2 text-right text-sm"
>
<
IconButton
color=
"danger"
variant=
"plain"
size=
"sm"
onClick=
{
()
=>
{
handleDeleteWebhook
(
webhook
);
}
}
>
<
Icon
.
Trash
className=
"w-4 h-auto"
/>
</
IconButton
>
</
td
>
</
tr
>
))
}
{
webhooks
.
length
===
0
&&
(
<
tr
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-gray-900 dark:text-gray-400"
colSpan=
{
3
}
>
No webhooks found.
</
td
>
</
tr
>
)
}
</
tbody
>
</
table
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</>
);
};
export
default
WebhookSection
;
web/src/grpcweb.ts
View file @
0026f9e5
...
...
@@ -6,6 +6,7 @@ import { ResourceServiceDefinition } from "./types/proto/api/v2/resource_service
import
{
SystemServiceDefinition
}
from
"./types/proto/api/v2/system_service"
;
import
{
TagServiceDefinition
}
from
"./types/proto/api/v2/tag_service"
;
import
{
UserServiceDefinition
}
from
"./types/proto/api/v2/user_service"
;
import
{
WebhookServiceDefinition
}
from
"./types/proto/api/v2/webhook_service"
;
const
channel
=
createChannel
(
window
.
location
.
origin
,
...
...
@@ -29,3 +30,5 @@ export const tagServiceClient = clientFactory.create(TagServiceDefinition, chann
export
const
inboxServiceClient
=
clientFactory
.
create
(
InboxServiceDefinition
,
channel
);
export
const
activityServiceClient
=
clientFactory
.
create
(
ActivityServiceDefinition
,
channel
);
export
const
webhookServiceClient
=
clientFactory
.
create
(
WebhookServiceDefinition
,
channel
);
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