Skip to content

Commit dff5744

Browse files
committed
feat: add nexus-gate service deployment logic
1 parent bcfc04a commit dff5744

12 files changed

Lines changed: 689 additions & 32 deletions

File tree

app/(model)/(report)/finish/service-info.tsx

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import * as React from 'react'
34
import { ReactNode } from 'react'
45
import { ModelIcon, OpenWebUI } from '@lobehub/icons'
56
import { useQuery } from '@tanstack/react-query'
@@ -9,7 +10,8 @@ import { AppCardSection, AppCardSectionTitle } from '@/components/app/app-card'
910
import { DescriptionsList } from '@/components/base/descriptions-list'
1011
import { CopyButton } from '@/app/(model)/(report)/copy-button'
1112
import { ModelDeployConfigType } from '@/app/(model)/select-model/schemas'
12-
import { OpenWebuiConfigType } from '@/app/(model)/service-config/schemas'
13+
import { NexusGateConfigType, OpenWebuiConfigType } from '@/app/(model)/service-config/schemas'
14+
import NexusGateLogo from '@/public/icons/nexus-gate.svg'
1315
import { useModelStore } from '@/stores/model-store-provider'
1416
import { useTRPC } from '@/trpc/client'
1517

@@ -22,6 +24,7 @@ export function ServiceInfo() {
2224
</AppCardSection>
2325
<AppCardSection>
2426
<AppCardSectionTitle>服务信息</AppCardSectionTitle>
27+
<NexusGateServicesInfo />
2528
<OpenWebuiServicesInfo />
2629
</AppCardSection>
2730
</>
@@ -114,14 +117,6 @@ function ModelHostDeployment({ deployment }: { deployment: ModelDeployConfigType
114117
</CopyButton>
115118
) : null,
116119
},
117-
{
118-
id: 'API 密钥',
119-
value: (
120-
<CopyButton value={deployment.apiKey} message="已复制 API 密钥">
121-
{deployment.apiKey}
122-
</CopyButton>
123-
),
124-
},
125120
]}
126121
/>
127122
</div>
@@ -175,3 +170,59 @@ function OpenWebuiInfo({ info }: { info: OpenWebuiConfigType }) {
175170
</div>
176171
)
177172
}
173+
174+
function NexusGateServicesInfo() {
175+
const deployment = useModelStore((s) => s.serviceDeploy.config.nexusGate)
176+
const info = Array.from(deployment.values())
177+
178+
if (deployment.size === 0) return null
179+
180+
return (
181+
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 rounded-lg border px-4 py-3">
182+
<div className="row-span-2 pt-1">
183+
<NexusGateLogo className="-mx-0.5 size-6" />
184+
</div>
185+
<h2 className="text-base font-semibold">Open WebUI</h2>
186+
{info.map((info) => (
187+
<NexusGateInfo key={info.host} info={info} />
188+
))}
189+
</div>
190+
)
191+
}
192+
193+
function NexusGateInfo({ info }: { info: NexusGateConfigType }) {
194+
const trpc = useTRPC()
195+
const { data: host } = useQuery(trpc.connection.getHostInfo.queryOptions(info.host))
196+
197+
if (!host) return null
198+
199+
const ipAddr = host?.ip[0]
200+
const url = ipAddr ? `http://${ipAddr}:${info.port}` : undefined
201+
202+
return (
203+
<div className="col-start-2">
204+
<div className="mb-1 text-sm font-medium">{host.info.system_info.hostname || ipAddr}</div>
205+
<DescriptionsList
206+
entries={[
207+
{
208+
id: 'url',
209+
key: '访问地址',
210+
value: (
211+
<a href={url} target="_blank" className="text-primary font-medium hover:underline">
212+
{url}
213+
</a>
214+
),
215+
},
216+
{
217+
id: '管理员密钥',
218+
value: (
219+
<CopyButton value={info.adminKey} message="已复制管理员密钥">
220+
{info.adminKey}
221+
</CopyButton>
222+
),
223+
},
224+
]}
225+
/>
226+
</div>
227+
)
228+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
'use client'
2+
3+
import * as React from 'react'
4+
import { AlertCircleIcon, CheckCircle2Icon, CheckIcon } from 'lucide-react'
5+
import { match } from 'ts-pattern'
6+
7+
import { isCompleted, useProgress } from '@/lib/progress/utils'
8+
import { AppCardSection } from '@/components/app/app-card'
9+
import { Callout } from '@/components/base/callout'
10+
import { Button } from '@/components/ui/button'
11+
import { Skeleton } from '@/components/ui/skeleton'
12+
import { Spinner } from '@/components/ui/spinner'
13+
import { TextCopyButton } from '@/app/(model)/text-copy-button'
14+
import NexusGateLogo from '@/public/icons/nexus-gate.svg'
15+
import { useModelStore } from '@/stores/model-store-provider'
16+
17+
import { ProgressIndicator } from '../progress-indicator'
18+
import { useServiceDeployContext } from '../service-deploy-provider'
19+
import { useHostInfo } from '../use-host-info'
20+
21+
export function NexusGateDeployPage() {
22+
const { deployMutation } = useServiceDeployContext()
23+
const isSuccess = useModelStore(
24+
(s) =>
25+
s.serviceDeploy.progress.nexusGate.size > 0 && s.serviceDeploy.progress.nexusGate.values().every(isCompleted),
26+
)
27+
28+
if (deployMutation.isError) {
29+
return (
30+
<AppCardSection>
31+
<Callout
32+
size="card"
33+
action={
34+
<Button variant="outline" size="xs" onClick={() => deployMutation.mutate()}>
35+
重试
36+
</Button>
37+
}
38+
>
39+
{deployMutation.error.message}
40+
</Callout>
41+
</AppCardSection>
42+
)
43+
}
44+
45+
return (
46+
<AppCardSection>
47+
<HostsList />
48+
{isSuccess && <DeploySuccessCallout />}
49+
</AppCardSection>
50+
)
51+
}
52+
53+
function HostsList() {
54+
const configs = useModelStore((s) => s.serviceDeploy.config.nexusGate)
55+
const hosts = Array.from(configs.values())
56+
return (
57+
<div className="grid gap-4">
58+
{hosts.map(({ host }) => (
59+
<HostStatusCard key={host} hostId={host} />
60+
))}
61+
</div>
62+
)
63+
}
64+
65+
function HostStatusCard({ hostId }: { hostId: string }) {
66+
const { data: host } = useHostInfo({ hostId })
67+
const config = useModelStore((s) => s.serviceDeploy.config.nexusGate.get(hostId))
68+
const deployProgress = useModelStore((s) => s.serviceDeploy.progress.nexusGate.get(hostId))
69+
const progress = useProgress(deployProgress)
70+
71+
const { deployOneMutation } = useServiceDeployContext()
72+
73+
if (!progress || !config) return null
74+
75+
const ipAddr = host?.ip[0]
76+
const url = ipAddr ? `http://${ipAddr}:${config.port}` : undefined
77+
78+
return (
79+
<div className="grid grid-cols-[1fr_auto] gap-y-1 rounded-xl border px-4 py-3">
80+
<div className="flex items-baseline gap-3">
81+
<h4 className="text-base font-medium">
82+
{host?.info.system_info.hostname ?? <Skeleton className="h-6 w-32" />}
83+
</h4>
84+
<div className="text-muted-foreground text-sm">{host?.ip[0]}</div>
85+
</div>
86+
<div className="col-start-2 row-span-2 pt-1">
87+
<NexusGateLogo className="-m-0.5 size-8" />
88+
</div>
89+
<div>NexusGate</div>
90+
<ProgressIndicator progress={progress} />
91+
<div className="text-muted-foreground flex gap-2 [&_svg]:size-4 [&_svg]:shrink-0 [&>svg]:translate-y-0.5">
92+
{match(progress.status)
93+
.with('done', () => (
94+
<>
95+
<CheckIcon className="text-success" />
96+
<div className="text-success">{progress.message}</div>
97+
</>
98+
))
99+
.with('running', () => (
100+
<>
101+
<Spinner />
102+
<div>{progress.message}</div>
103+
</>
104+
))
105+
.with('error', () => (
106+
<>
107+
<AlertCircleIcon className="text-destructive" />
108+
<div className="text-destructive">{progress.message}</div>
109+
<button
110+
className="text-primary hover:text-primary/90 shrink-0 font-medium whitespace-nowrap"
111+
onClick={() => deployOneMutation.mutate({ host: hostId, service: 'nexusGate' })}
112+
>
113+
重试
114+
</button>
115+
</>
116+
))
117+
.with('idle', () => <div>{progress.message}</div>)
118+
.exhaustive()}
119+
</div>
120+
{url && progress.status === 'done' && (
121+
<div className="col-span-full flex">
122+
<div className="text-foreground border-border/50 grid grid-cols-[auto_auto] gap-x-2.5 gap-y-1 rounded-md border px-3 py-2.5">
123+
<dl className="contents">
124+
<dt className="text-muted-foreground">服务访问地址</dt>
125+
<dd>
126+
<a href={url} target="_blank" className="text-primary font-medium hover:underline">
127+
{url}
128+
</a>
129+
</dd>
130+
</dl>
131+
<dl className="contents">
132+
<dt className="text-muted-foreground">管理员密钥</dt>
133+
<dd>
134+
<TextCopyButton value={config.adminKey} message="已复制管理员密钥">
135+
{config.adminKey}
136+
</TextCopyButton>
137+
</dd>
138+
</dl>
139+
</div>
140+
</div>
141+
)}
142+
</div>
143+
)
144+
}
145+
146+
function DeploySuccessCallout() {
147+
return (
148+
<Callout size="card" variant="success" icon={<CheckCircle2Icon />}>
149+
NexusGate 部署完成
150+
</Callout>
151+
)
152+
}

app/(model)/deploy-service/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AppCardContent, AppCardHeader, AppCardTitle } from '@/components/app/ap
22
import { AppFrame } from '@/components/app/app-frame'
33

44
import { Footer } from './footer'
5+
import { NexusGateDeployPage } from './nexus-gate-deploy-page'
56
import { OpenWebuiDeployPage } from './open-webui-deploy-page'
67

78
export default function Page() {
@@ -11,6 +12,7 @@ export default function Page() {
1112
<AppCardTitle>部署预置服务</AppCardTitle>
1213
</AppCardHeader>
1314
<AppCardContent>
15+
<NexusGateDeployPage />
1416
<OpenWebuiDeployPage />
1517
</AppCardContent>
1618
<Footer />

app/(model)/service-config/footer.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,33 @@
11
'use client'
22

3+
import { match, P } from 'ts-pattern'
4+
35
import { AppCardFooter } from '@/components/app/app-card'
46
import { NavButton } from '@/components/base/nav-button'
57
import { NavButtonGuard } from '@/components/base/nav-button-guard'
6-
import { useServiceDeployContext } from '@/app/(model)/service-deploy-provider'
78
import { useModelStore } from '@/stores/model-store-provider'
89

10+
import { useServiceDeployContext } from '../service-deploy-provider'
11+
912
export function Footer() {
10-
const hasConfig = useModelStore((s) => {
11-
return Object.values(s.serviceDeploy.config).some((map) => map.size > 0)
12-
})
13+
const hasNexusGateConfig = useModelStore((s) => s.serviceDeploy.config.nexusGate.size > 0)
14+
const isOtherServiceValid = useModelStore((s) =>
15+
new Set(s.serviceDeploy.config.openWebui.keys()).isSubsetOf(new Set(s.serviceDeploy.config.nexusGate.keys())),
16+
)
1317
const { deployMutation } = useServiceDeployContext()
1418

19+
const message = match([hasNexusGateConfig, isOtherServiceValid])
20+
.with([false, P.boolean], () => '请至少添加一个 NexusGate 配置')
21+
.with([true, false], () => 'NexusGate 是其他服务的前置服务,请添加 NexusGate 配置')
22+
.with([true, true], () => null)
23+
.exhaustive()
24+
1525
return (
1626
<AppCardFooter>
1727
<NavButton variant="outline" to="/deploy-model">
1828
上一步
1929
</NavButton>
20-
<NavButtonGuard pass={hasConfig} message="请至少添加一个配置">
30+
<NavButtonGuard pass={hasNexusGateConfig && isOtherServiceValid} message={message}>
2131
<NavButton to="/deploy-service" onClick={() => deployMutation.mutate()}>
2232
开始部署
2333
</NavButton>

0 commit comments

Comments
 (0)