Skip to content

Commit

Permalink
feat: sse notify
Browse files Browse the repository at this point in the history
  • Loading branch information
0xJacky committed Nov 4, 2024
1 parent b4add42 commit e6e1876
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 47 deletions.
31 changes: 31 additions & 0 deletions api/notification/live.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package notification

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

func Live(c *gin.Context) {
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")

evtChan := make(chan *model.Notification)

notification.SetClient(c, evtChan)

notify := c.Writer.CloseNotify()
go func() {
<-notify
notification.RemoveClient(c)
}()

for n := range evtChan {
c.Stream(func(w io.Writer) bool {
c.SSEvent("message", n)
return false
})
}
}
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.

62 changes: 56 additions & 6 deletions app/src/components/Notification/Notification.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,71 @@
<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
s.onabort = 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 +87,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 +97,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 +119,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

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
40 changes: 38 additions & 2 deletions internal/notification/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,29 @@ package notification
import (
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/uozi-tech/cosy/logger"
"sync"
)

var (
clientMap = make(map[*gin.Context]chan *model.Notification)
mutex = &sync.RWMutex{}
)

func SetClient(c *gin.Context, evtChan chan *model.Notification) {
mutex.Lock()
defer mutex.Unlock()
clientMap[c] = evtChan
}

func RemoveClient(c *gin.Context) {
mutex.Lock()
defer mutex.Unlock()
close(clientMap[c])
delete(clientMap, c)
}

func Info(title string, details string) {
push(model.NotificationInfo, title, details)
}
Expand All @@ -24,9 +45,24 @@ func Success(title string, details string) {
func push(nType model.NotificationType, title string, details string) {
n := query.Notification

_ = n.Create(&model.Notification{
data := &model.Notification{
Type: nType,
Title: title,
Details: details,
})
}

err := n.Create(data)
if err != nil {
logger.Error(err)
return
}
broadcast(data)
}

func broadcast(data *model.Notification) {
mutex.RLock()
defer mutex.RUnlock()
for _, evtChan := range clientMap {
evtChan <- data
}
}
1 change: 1 addition & 0 deletions internal/site/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type SyncResult struct {
Name string `json:"name"`
NewName string `json:"new_name,omitempty"`
Response gin.H `json:"response"`
Error string `json:"error"`
}

func NewSyncResult(node string, siteName string, resp *resty.Response) (s *SyncResult) {
Expand Down

0 comments on commit e6e1876

Please sign in to comment.