mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 02:15:48 +02:00
commit
96dfbd0025
11 changed files with 187 additions and 47 deletions
31
api/notification/live.go
Normal file
31
api/notification/live.go
Normal file
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
8
app/pnpm-lock.yaml
generated
8
app/pnpm-lock.yaml
generated
|
@ -83,6 +83,9 @@ importers:
|
|||
sortablejs:
|
||||
specifier: ^1.15.3
|
||||
version: 1.15.3
|
||||
sse.js:
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
universal-cookie:
|
||||
specifier: ^7.2.2
|
||||
version: 7.2.2
|
||||
|
@ -4271,6 +4274,9 @@ packages:
|
|||
spdx-license-ids@3.0.20:
|
||||
resolution: {integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==}
|
||||
|
||||
sse.js@2.5.0:
|
||||
resolution: {integrity: sha512-I7zYndqOOkNpz9KIdFZ8c8A7zs1YazNewBr8Nsi/tqThfJkVPuP1q7UE2h4B0RwoWZxbBYpd06uoW3NI3SaZXg==}
|
||||
|
||||
stable-hash@0.0.4:
|
||||
resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==}
|
||||
|
||||
|
@ -9690,6 +9696,8 @@ snapshots:
|
|||
|
||||
spdx-license-ids@3.0.20: {}
|
||||
|
||||
sse.js@2.5.0: {}
|
||||
|
||||
stable-hash@0.0.4: {}
|
||||
|
||||
std-env@3.7.0: {}
|
||||
|
|
|
@ -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 => {
|
||||
|
@ -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
|
||||
|
@ -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 => {
|
||||
|
@ -70,6 +119,7 @@ function viewAll() {
|
|||
placement="bottomRight"
|
||||
overlay-class-name="notification-popover"
|
||||
trigger="click"
|
||||
:get-popup-container="() => headerRef"
|
||||
>
|
||||
<ABadge
|
||||
:count="unreadCount"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
return args.text
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
@ -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>
|
||||
|
@ -37,7 +40,7 @@ function logout() {
|
|||
|
||||
<SwitchAppearance />
|
||||
|
||||
<Notification />
|
||||
<Notification :header-ref="headerRef" />
|
||||
|
||||
<NginxControl />
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue