Skip to content

Commit

Permalink
Merge pull request #706 from 0xJacky/feat/sse-notify
Browse files Browse the repository at this point in the history
enhance(sse): add X-Accel-Buffering header

Merge pull request #697 from 0xJacky/feat/sse-notify

feat: sse notify
  • Loading branch information
Hintay committed Nov 15, 2024
2 parents d456025 + 31be0b2 commit 147b35b
Show file tree
Hide file tree
Showing 16 changed files with 207 additions and 122 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Yet another Nginx Web UI, developed by [0xJacky](https://jackyu.cn/) and [Hintay
[![Translated Status](https://weblate.nginxui.com/widget/nginx-ui/frontend/svg-badge.svg)](https://weblate.nginxui.com/engage/nginx-ui/)
[![Featured|HelloGitHub](https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=86f3a8f779934748a34fe6f1b5cd442f&claim_uid=MOFqadzAShCBeQj&theme=small)](https://hellogithub.com/repository/86f3a8f779934748a34fe6f1b5cd442f)

<iframe src="https://github.com/sponsors/nginxui/card" title="Sponsor Nginx UI" height="225" width="600" style="border: 0;"></iframe>

## Documentation
To check out docs, visit [nginxui.com](https://nginxui.com).
Expand Down
46 changes: 46 additions & 0 deletions api/notification/live.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package notification

import (
"github.com/0xJacky/Nginx-UI/internal/notification"
"github.com/0xJacky/Nginx-UI/model"
"github.com/gin-gonic/gin"
"io"
"time"
)

func Live(c *gin.Context) {
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
// https://stackoverflow.com/questions/27898622/server-sent-events-stopped-work-after-enabling-ssl-on-proxy/27960243#27960243
c.Header("X-Accel-Buffering", "no")

evtChan := make(chan *model.Notification)

notification.SetClient(c, evtChan)

notify := c.Writer.CloseNotify()

c.Stream(func(w io.Writer) bool {
c.SSEvent("heartbeat", "")
return false
})

for {
select {
case n := <-evtChan:
c.Stream(func(w io.Writer) bool {
c.SSEvent("message", n)
return false
})
case <-time.After(30 * time.Second):
c.Stream(func(w io.Writer) bool {
c.SSEvent("heartbeat", "")
return false
})
case <-notify:
notification.RemoveClient(c)
return
}
}
}
2 changes: 2 additions & 0 deletions api/notification/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ func InitRouter(r *gin.RouterGroup) {
r.GET("notifications/:id", Get)
r.DELETE("notifications/:id", Destroy)
r.DELETE("notifications", DestroyAll)

r.GET("notifications/live", Live)
}
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"pinia-plugin-persistedstate": "^4.1.2",
"reconnecting-websocket": "^4.4.0",
"sortablejs": "^1.15.3",
"sse.js": "^2.5.0",
"universal-cookie": "^7.2.2",
"unocss": "^0.63.6",
"vite-plugin-build-id": "0.5.0",
Expand Down
8 changes: 8 additions & 0 deletions app/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 55 additions & 6 deletions app/src/components/Notification/Notification.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,70 @@
<script setup lang="ts">
import type { Notification } from '@/api/notification'
import type { CustomRenderProps } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
import type { SSEvent } from 'sse.js'
import type { Ref } from 'vue'
import notification from '@/api/notification'
import notificationApi from '@/api/notification'
import { detailRender } from '@/components/Notification/detailRender'
import { NotificationTypeT } from '@/constants'
import { useUserStore } from '@/pinia'
import { BellOutlined, CheckCircleOutlined, CloseCircleOutlined, DeleteOutlined, InfoCircleOutlined, WarningOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { message, notification } from 'ant-design-vue'
import { SSE } from 'sse.js'
defineProps<{
headerRef: HTMLElement
}>()
const loading = ref(false)
const { unreadCount } = storeToRefs(useUserStore())
const { token, unreadCount } = storeToRefs(useUserStore())
const data = ref([]) as Ref<Notification[]>
const sse = shallowRef(newSSE())
function reconnect() {
setTimeout(() => {
sse.value = newSSE()
}, 5000)
}
function newSSE() {
const s = new SSE('/api/notifications/live', {
headers: {
Authorization: token.value,
},
})
s.onmessage = (e: SSEvent) => {
const data = JSON.parse(e.data)
// data.type may be 0
if (data.type === undefined || data.type === null || data.type === '') {
return
}
const typeTrans = {
0: 'error',
1: 'warning',
2: 'info',
3: 'success',
}
notification[typeTrans[data.type]]({
message: $gettext(data.title),
description: detailRender({ text: data.details, record: data } as CustomRenderProps),
})
}
// reconnect
s.onerror = reconnect
return s
}
function init() {
loading.value = true
notification.get_list().then(r => {
notificationApi.get_list().then(r => {
data.value = r.data
unreadCount.value = r.pagination?.total || 0
}).catch(e => {
Expand All @@ -38,7 +86,7 @@ watch(open, v => {
})
function clear() {
notification.clear().then(() => {
notificationApi.clear().then(() => {
message.success($gettext('Cleared successfully'))
data.value = []
unreadCount.value = 0
Expand All @@ -48,7 +96,7 @@ function clear() {
}
function remove(id: number) {
notification.destroy(id).then(() => {
notificationApi.destroy(id).then(() => {
message.success($gettext('Removed successfully'))
init()
}).catch(e => {
Expand All @@ -70,6 +118,7 @@ function viewAll() {
placement="bottomRight"
overlay-class-name="notification-popover"
trigger="click"
:get-popup-container="() => headerRef"
>
<ABadge
:count="unreadCount"
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/Notification/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function syncRenameConfigError(text: string) {

export function saveSiteSuccess(text: string) {
const data = JSON.parse(text)
return $gettext('Save Site %{site} to %{node} successfully', { site: data.site, node: data.node })
return $gettext('Save Site %{site} to %{node} successfully', { site: data.name, node: data.node })
}

export function saveSiteError(text: string) {
Expand Down
76 changes: 41 additions & 35 deletions app/src/components/Notification/detailRender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,42 +18,48 @@ import {
} from '@/components/Notification/config'

export function detailRender(args: CustomRenderProps) {
switch (args.record.title) {
case 'Sync Certificate Success':
return syncCertificateSuccess(args.text)
case 'Sync Certificate Error':
return syncCertificateError(args.text)
case 'Rename Remote Config Success':
return syncRenameConfigSuccess(args.text)
case 'Rename Remote Config Error':
return syncRenameConfigError(args.text)
try {
switch (args.record.title) {
case 'Sync Certificate Success':
return syncCertificateSuccess(args.text)
case 'Sync Certificate Error':
return syncCertificateError(args.text)
case 'Rename Remote Config Success':
return syncRenameConfigSuccess(args.text)
case 'Rename Remote Config Error':
return syncRenameConfigError(args.text)

case 'Save Remote Site Success':
return saveSiteSuccess(args.text)
case 'Save Remote Site Error':
return saveSiteError(args.text)
case 'Delete Remote Site Success':
return deleteSiteSuccess(args.text)
case 'Delete Remote Site Error':
return deleteSiteError(args.text)
case 'Enable Remote Site Success':
return enableSiteSuccess(args.text)
case 'Enable Remote Site Error':
return enableSiteError(args.text)
case 'Disable Remote Site Success':
return disableSiteSuccess(args.text)
case 'Disable Remote Site Error':
return disableSiteError(args.text)
case 'Rename Remote Site Success':
return renameSiteSuccess(args.text)
case 'Rename Remote Site Error':
return renameSiteError(args.text)
case 'Save Remote Site Success':
return saveSiteSuccess(args.text)
case 'Save Remote Site Error':
return saveSiteError(args.text)
case 'Delete Remote Site Success':
return deleteSiteSuccess(args.text)
case 'Delete Remote Site Error':
return deleteSiteError(args.text)
case 'Enable Remote Site Success':
return enableSiteSuccess(args.text)
case 'Enable Remote Site Error':
return enableSiteError(args.text)
case 'Disable Remote Site Success':
return disableSiteSuccess(args.text)
case 'Disable Remote Site Error':
return disableSiteError(args.text)
case 'Rename Remote Site Success':
return renameSiteSuccess(args.text)
case 'Rename Remote Site Error':
return renameSiteError(args.text)

case 'Sync Config Success':
return syncConfigSuccess(args.text)
case 'Sync Config Error':
return syncConfigError(args.text)
default:
return args.text
case 'Sync Config Success':
return syncConfigSuccess(args.text)
case 'Sync Config Error':
return syncConfigError(args.text)
default:
return args.text
}
}
// eslint-disable-next-line sonarjs/no-ignored-exceptions
catch (e) {

Check warning on line 62 in app/src/components/Notification/detailRender.ts

View workflow job for this annotation

GitHub Actions / build_app

'e' is defined but never used
return args.text
}
}
4 changes: 3 additions & 1 deletion app/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const PrivateKeyTypeMask = {
P384: 'EC384',
} as const

export const PrivateKeyTypeList = Object.entries(PrivateKeyTypeMask).map(([key, name]) => ({ key, name }))
export const PrivateKeyTypeList
= Object.entries(PrivateKeyTypeMask).map(([key, name]) =>
({ key, name }))

export type PrivateKeyType = keyof typeof PrivateKeyTypeMask
7 changes: 5 additions & 2 deletions app/src/layouts/HeaderLayout.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { ShallowRef } from 'vue'
import auth from '@/api/auth'
import NginxControl from '@/components/NginxControl/NginxControl.vue'
import Notification from '@/components/Notification/Notification.vue'
Expand All @@ -21,10 +22,12 @@ function logout() {
router.push('/login')
})
}
const headerRef = useTemplateRef('headerRef') as Readonly<ShallowRef<HTMLDivElement>>
</script>

<template>
<div class="header">
<div ref="headerRef" class="header">
<div class="tool">
<MenuUnfoldOutlined @click="emit('clickUnFold')" />
</div>
Expand All @@ -37,7 +40,7 @@ function logout() {

<SwitchAppearance />

<Notification />
<Notification :header-ref="headerRef" />

<NginxControl />

Expand Down
2 changes: 1 addition & 1 deletion app/src/version.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"2.0.0-beta.39","build_id":1,"total_build":368}
{"version":"2.0.0-beta.39","build_id":3,"total_build":370}
Loading

0 comments on commit 147b35b

Please sign in to comment.