Skip to content

Commit 552cf5c

Browse files
committed
feat(upstream): add upstream cards and detail modal
1 parent 27a6993 commit 552cf5c

File tree

29 files changed

+1666
-1126
lines changed

29 files changed

+1666
-1126
lines changed

.claude/settings.local.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(pnpm typecheck:*)",
5+
"Bash(pnpm lint:*)"
6+
],
7+
"deny": []
8+
}
9+
}

api/sites/site.go

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/0xJacky/Nginx-UI/internal/helper"
99
"github.com/0xJacky/Nginx-UI/internal/nginx"
1010
"github.com/0xJacky/Nginx-UI/internal/site"
11+
"github.com/0xJacky/Nginx-UI/internal/upstream"
1112
"github.com/0xJacky/Nginx-UI/model"
1213
"github.com/0xJacky/Nginx-UI/query"
1314
"github.com/gin-gonic/gin"
@@ -16,6 +17,38 @@ import (
1617
"gorm.io/gorm/clause"
1718
)
1819

20+
// buildProxyTargets processes proxy targets similar to list.go logic
21+
func buildProxyTargets(fileName string) []site.ProxyTarget {
22+
indexedSite := site.GetIndexedSite(fileName)
23+
24+
// Convert proxy targets, expanding upstream references
25+
var proxyTargets []site.ProxyTarget
26+
upstreamService := upstream.GetUpstreamService()
27+
28+
for _, target := range indexedSite.ProxyTargets {
29+
// Check if target.Host is an upstream name
30+
if upstreamDef, exists := upstreamService.GetUpstreamDefinition(target.Host); exists {
31+
// Replace with upstream servers
32+
for _, server := range upstreamDef.Servers {
33+
proxyTargets = append(proxyTargets, site.ProxyTarget{
34+
Host: server.Host,
35+
Port: server.Port,
36+
Type: server.Type,
37+
})
38+
}
39+
} else {
40+
// Regular proxy target
41+
proxyTargets = append(proxyTargets, site.ProxyTarget{
42+
Host: target.Host,
43+
Port: target.Port,
44+
Type: target.Type,
45+
})
46+
}
47+
}
48+
49+
return proxyTargets
50+
}
51+
1952
func GetSite(c *gin.Context) {
2053
name := helper.UnescapeURL(c.Param("name"))
2154

@@ -48,13 +81,14 @@ func GetSite(c *gin.Context) {
4881
}
4982

5083
c.JSON(http.StatusOK, site.Site{
51-
ModifiedAt: file.ModTime(),
52-
Site: siteModel,
53-
Name: name,
54-
Config: string(origContent),
55-
AutoCert: certModel.AutoCert == model.AutoCertEnabled,
56-
Filepath: path,
57-
Status: site.GetSiteStatus(name),
84+
ModifiedAt: file.ModTime(),
85+
Site: siteModel,
86+
Name: name,
87+
Config: string(origContent),
88+
AutoCert: certModel.AutoCert == model.AutoCertEnabled,
89+
Filepath: path,
90+
Status: site.GetSiteStatus(name),
91+
ProxyTargets: buildProxyTargets(name),
5892
})
5993
return
6094
}
@@ -80,15 +114,16 @@ func GetSite(c *gin.Context) {
80114
}
81115

82116
c.JSON(http.StatusOK, site.Site{
83-
Site: siteModel,
84-
ModifiedAt: file.ModTime(),
85-
Name: name,
86-
Config: nginxConfig.FmtCode(),
87-
Tokenized: nginxConfig,
88-
AutoCert: certModel.AutoCert == model.AutoCertEnabled,
89-
CertInfo: certInfoMap,
90-
Filepath: path,
91-
Status: site.GetSiteStatus(name),
117+
Site: siteModel,
118+
ModifiedAt: file.ModTime(),
119+
Name: name,
120+
Config: nginxConfig.FmtCode(),
121+
Tokenized: nginxConfig,
122+
AutoCert: certModel.AutoCert == model.AutoCertEnabled,
123+
CertInfo: certInfoMap,
124+
Filepath: path,
125+
Status: site.GetSiteStatus(name),
126+
ProxyTargets: buildProxyTargets(name),
92127
})
93128
}
94129

api/streams/streams.go

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/0xJacky/Nginx-UI/internal/helper"
99
"github.com/0xJacky/Nginx-UI/internal/nginx"
1010
"github.com/0xJacky/Nginx-UI/internal/stream"
11+
"github.com/0xJacky/Nginx-UI/internal/upstream"
1112
"github.com/0xJacky/Nginx-UI/model"
1213
"github.com/0xJacky/Nginx-UI/query"
1314
"github.com/gin-gonic/gin"
@@ -31,6 +32,38 @@ type Stream struct {
3132
ProxyTargets []config.ProxyTarget `json:"proxy_targets,omitempty"`
3233
}
3334

35+
// buildProxyTargets processes stream proxy targets similar to list.go logic
36+
func buildStreamProxyTargets(fileName string) []config.ProxyTarget {
37+
indexedStream := stream.GetIndexedStream(fileName)
38+
39+
// Convert proxy targets, expanding upstream references
40+
var proxyTargets []config.ProxyTarget
41+
upstreamService := upstream.GetUpstreamService()
42+
43+
for _, target := range indexedStream.ProxyTargets {
44+
// Check if target.Host is an upstream name
45+
if upstreamDef, exists := upstreamService.GetUpstreamDefinition(target.Host); exists {
46+
// Replace with upstream servers
47+
for _, server := range upstreamDef.Servers {
48+
proxyTargets = append(proxyTargets, config.ProxyTarget{
49+
Host: server.Host,
50+
Port: server.Port,
51+
Type: server.Type,
52+
})
53+
}
54+
} else {
55+
// Regular proxy target
56+
proxyTargets = append(proxyTargets, config.ProxyTarget{
57+
Host: target.Host,
58+
Port: target.Port,
59+
Type: target.Type,
60+
})
61+
}
62+
}
63+
64+
return proxyTargets
65+
}
66+
3467
func GetStreams(c *gin.Context) {
3568
// Parse query parameters
3669
options := &stream.ListOptions{
@@ -101,14 +134,15 @@ func GetStream(c *gin.Context) {
101134

102135
// Build response based on advanced mode
103136
response := Stream{
104-
ModifiedAt: info.FileInfo.ModTime(),
105-
Advanced: info.Model.Advanced,
106-
Status: info.Status,
107-
Name: name,
108-
Filepath: info.Path,
109-
EnvGroupID: info.Model.EnvGroupID,
110-
EnvGroup: info.Model.EnvGroup,
111-
SyncNodeIDs: info.Model.SyncNodeIDs,
137+
ModifiedAt: info.FileInfo.ModTime(),
138+
Advanced: info.Model.Advanced,
139+
Status: info.Status,
140+
Name: name,
141+
Filepath: info.Path,
142+
EnvGroupID: info.Model.EnvGroupID,
143+
EnvGroup: info.Model.EnvGroup,
144+
SyncNodeIDs: info.Model.SyncNodeIDs,
145+
ProxyTargets: buildStreamProxyTargets(name),
112146
}
113147

114148
if info.Model.Advanced {

app/components.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ declare module 'vue' {
122122
SyncNodesPreviewSyncNodesPreview: typeof import('./src/components/SyncNodesPreview/SyncNodesPreview.vue')['default']
123123
SystemRestoreSystemRestoreContent: typeof import('./src/components/SystemRestore/SystemRestoreContent.vue')['default']
124124
TwoFAAuthorization: typeof import('./src/components/TwoFA/Authorization.vue')['default']
125+
UpstreamCardsUpstreamCards: typeof import('./src/components/UpstreamCards/UpstreamCards.vue')['default']
126+
UpstreamDetailModalUpstreamDetailModal: typeof import('./src/components/UpstreamDetailModal/UpstreamDetailModal.vue')['default']
125127
VPSwitchVPSwitch: typeof import('./src/components/VPSwitch/VPSwitch.vue')['default']
126128
}
127129
}

app/src/components/NgxConfigEditor/NgxUpstream.vue

Lines changed: 3 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
<script setup lang="ts">
2-
import type ReconnectingWebSocket from 'reconnecting-websocket'
3-
import type { NgxDirective } from '@/api/ngx'
4-
import type { UpstreamStatus } from '@/api/upstream'
52
import { MoreOutlined, PlusOutlined } from '@ant-design/icons-vue'
63
import { Modal } from 'ant-design-vue'
7-
import { throttle } from 'lodash'
8-
import upstream from '@/api/upstream'
9-
import { DirectiveEditor, Server, useNgxConfigStore } from '.'
4+
import { DirectiveEditor, useNgxConfigStore } from '.'
105
116
const [modal, ContextHolder] = Modal.useModal()
127
@@ -42,10 +37,6 @@ function removeUpstream(index: number) {
4237
})
4338
}
4439
45-
const curUptreamDirectives = computed(() => {
46-
return ngxConfig.value.upstreams?.[currentUpstreamIdx.value]?.directives
47-
})
48-
4940
const open = ref(false)
5041
const renameIdx = ref(-1)
5142
const buffer = ref('')
@@ -61,53 +52,6 @@ function renameOK() {
6152
ngxConfig.value.upstreams[renameIdx.value].name = buffer.value
6253
open.value = false
6354
}
64-
65-
const availabilityResult = ref({}) as Ref<Record<string, UpstreamStatus>>
66-
const websocket = shallowRef<ReconnectingWebSocket | WebSocket>()
67-
68-
function availabilityTest() {
69-
const sockets: string[] = []
70-
for (const u of ngxConfig.value.upstreams ?? []) {
71-
for (const d of u.directives ?? []) {
72-
if (d.directive === Server)
73-
sockets.push(d.params.split(' ')[0])
74-
}
75-
}
76-
77-
if (sockets.length > 0) {
78-
websocket.value = upstream.availabilityWebSocket()
79-
websocket.value.onopen = () => {
80-
websocket.value!.send(JSON.stringify(sockets))
81-
}
82-
websocket.value.onmessage = (e: MessageEvent) => {
83-
availabilityResult.value = JSON.parse(e.data)
84-
}
85-
}
86-
}
87-
88-
onMounted(() => {
89-
availabilityTest()
90-
})
91-
92-
onBeforeUnmount(() => {
93-
websocket.value?.close()
94-
})
95-
96-
async function _restartTest() {
97-
websocket.value?.close()
98-
availabilityTest()
99-
}
100-
101-
const restartTest = throttle(_restartTest, 5000)
102-
103-
watch(curUptreamDirectives, () => {
104-
restartTest()
105-
}, { deep: true })
106-
107-
function getAvailabilityResult(directive: NgxDirective) {
108-
const params = directive.params.split(' ')
109-
return availabilityResult.value[params?.[0]]
110-
}
11155
</script>
11256

11357
<template>
@@ -139,20 +83,7 @@ function getAvailabilityResult(directive: NgxDirective) {
13983
</template>
14084

14185
<div class="tab-content">
142-
<DirectiveEditor v-model:directives="v.directives">
143-
<template #directiveSuffix="{ directive }: {directive: NgxDirective}">
144-
<template v-if="directive.directive === Server">
145-
<template v-if="getAvailabilityResult(directive)?.online">
146-
<ABadge color="green" />
147-
{{ getAvailabilityResult(directive)?.latency?.toFixed(2) }}ms
148-
</template>
149-
<template v-else>
150-
<ABadge color="red" />
151-
{{ $gettext('Offline') }}
152-
</template>
153-
</template>
154-
</template>
155-
</DirectiveEditor>
86+
<DirectiveEditor v-model:directives="v.directives" />
15687
</div>
15788
</ATabPane>
15889

@@ -207,7 +138,7 @@ function getAvailabilityResult(directive: NgxDirective) {
207138
<style scoped lang="less">
208139
.empty-state {
209140
@apply px-8 text-center;
210-
min-height: 400px;
141+
min-height: 200px;
211142
display: flex;
212143
flex-direction: column;
213144
justify-content: center;

0 commit comments

Comments
 (0)