Skip to content

Commit a8aea4b

Browse files
committed
feat: add mirrors select on confirming installation
1 parent a852150 commit a8aea4b

File tree

7 files changed

+178
-36
lines changed

7 files changed

+178
-36
lines changed

app/install-env/ssh/atoms.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (c) 2025 EM-GeekLab
3+
* LLMOne is licensed under Mulan PSL v2.
4+
* You can use this software according to the terms and conditions of the Mulan PSL v2.
5+
* You may obtain a copy of Mulan PSL v2 at:
6+
* http://license.coscl.org.cn/MulanPSL2
7+
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
8+
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
9+
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
10+
* See the Mulan PSL v2 for more details.
11+
*/
12+
13+
import { enableMapSet } from 'immer'
14+
import { atom } from 'jotai'
15+
16+
import type { InstallOptions } from '@/lib/ssh/ssh-deployer'
17+
18+
enableMapSet()
19+
20+
export const installConfigAtom = atom<Map<string, InstallOptions>>(new Map())

app/install-env/ssh/hooks.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
* See the Mulan PSL v2 for more details.
1111
*/
1212

13+
import { useCallback } from 'react'
1314
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
15+
import { useAtomValue } from 'jotai/react'
1416

17+
import { installConfigAtom } from '@/app/install-env/ssh/atoms'
1518
import { useGlobalStoreApi } from '@/stores'
1619
import { useTRPC, useTRPCClient } from '@/trpc/client'
1720

@@ -20,13 +23,14 @@ export function useInstallStartTrigger(): { start: () => void } {
2023
const trpcClient = useTRPCClient()
2124
const trpc = useTRPC()
2225
const queryClient = useQueryClient()
26+
const installConfigs = useAtomValue(installConfigAtom)
2327

2428
const { mutate: start } = useMutation({
2529
mutationFn: async () => {
2630
const { finalSshHosts } = storeApi.getState()
2731
await Promise.all(
2832
finalSshHosts.map(async ({ ip: host }) => {
29-
await trpcClient.sshDeploy.install.triggerOnce.mutate(host)
33+
await trpcClient.sshDeploy.install.triggerOnce.mutate({ host, options: installConfigs.get(host) })
3034
}),
3135
)
3236
},
@@ -47,10 +51,11 @@ export function useInstallStartTrigger(): { start: () => void } {
4751
export function useInstallRetryTrigger(): { retry: (host: string) => void } {
4852
const trpc = useTRPC()
4953
const queryClient = useQueryClient()
54+
const installConfigs = useAtomValue(installConfigAtom)
5055

51-
const { mutate: retry } = useMutation(
56+
const { mutate } = useMutation(
5257
trpc.sshDeploy.install.triggerOnce.mutationOptions({
53-
onSuccess: (_, host) => {
58+
onSuccess: (_, { host }) => {
5459
return Promise.all([
5560
queryClient.resetQueries({ queryKey: trpc.sshDeploy.install.status.queryKey(host) }),
5661
queryClient.resetQueries({ queryKey: trpc.sshDeploy.install.statusAll.queryKey() }),
@@ -59,6 +64,11 @@ export function useInstallRetryTrigger(): { retry: (host: string) => void } {
5964
}),
6065
)
6166

67+
const retry = useCallback(
68+
(host: string) => mutate({ host, options: installConfigs.get(host) }),
69+
[installConfigs, mutate],
70+
)
71+
6272
return { retry }
6373
}
6474

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use client'
2+
3+
import { Provider } from 'jotai/react'
4+
5+
export function JotaiProvider({ children }: { children: React.ReactNode }) {
6+
return <Provider>{children}</Provider>
7+
}

app/install-env/ssh/ssh-hosts-confirm.tsx

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,20 @@
1313
'use client'
1414

1515
import * as React from 'react'
16-
import { ReactNode } from 'react'
16+
import { ComponentProps, ReactNode, useId } from 'react'
1717
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
18+
import { useAtom } from 'jotai/react'
1819
import { AlertTriangleIcon, ArrowRightIcon, CheckIcon } from 'lucide-react'
1920
import { match } from 'ts-pattern'
2021

21-
import type { InstallFlag, SshDeployerInfo } from '@/lib/ssh/ssh-deployer'
22+
import type { InstallFlag, InstallOptions, SshDeployerInfo } from '@/lib/ssh/ssh-deployer'
23+
import { cn } from '@/lib/utils'
2224
import { AppCardContent, AppCardFooter, AppCardSection } from '@/components/app/app-card'
2325
import { Callout } from '@/components/base/callout'
2426
import { NavButton } from '@/components/base/nav-button'
2527
import { Badge } from '@/components/ui/badge'
2628
import { Button } from '@/components/ui/button'
29+
import { installConfigAtom } from '@/app/install-env/ssh/atoms'
2730
import { useGlobalStore } from '@/stores'
2831
import { useTRPC, useTRPCClient } from '@/trpc/client'
2932

@@ -112,13 +115,17 @@ function HostConfirmList({ host }: { host: SshDeployerInfo }) {
112115
flag={flags.updateSources}
113116
plannedMessage="即将更新软件源镜像"
114117
completedMessage="已更新软件源镜像"
115-
/>
118+
>
119+
<PackageMirrorSelect host={host.host} />
120+
</HostConfirmItem>
116121
<HostConfirmItem
117122
flag={flags.installDependencies}
118123
plannedMessage="即将安装基础依赖"
119124
completedMessage="已安装基础依赖"
120125
/>
121-
<HostConfirmItem flag={flags.installDocker} plannedMessage="即将安装 Docker" completedMessage="已安装 Docker" />
126+
<HostConfirmItem flag={flags.installDocker} plannedMessage="即将安装 Docker" completedMessage="已安装 Docker">
127+
<DockerPackageMirrorSelect host={host.host} />
128+
</HostConfirmItem>
122129
{flags.installNvidiaGpu && (
123130
<HostConfirmItem
124131
flag={flags.installNvidiaGpu}
@@ -145,6 +152,68 @@ function HostConfirmList({ host }: { host: SshDeployerInfo }) {
145152
)
146153
}
147154

155+
function PackageMirrorSelect({ host }: { host: string }) {
156+
const id = useId()
157+
const [options, setOptions] = useAtom(installConfigAtom)
158+
const value = options.get(host)?.packageMirror || 'none'
159+
const setValue = (e: React.ChangeEvent<HTMLSelectElement>) => {
160+
const newMirror = e.target.value as InstallOptions['packageMirror']
161+
setOptions((prev) => {
162+
const newOptions = new Map(prev)
163+
const currentConfig = newOptions.get(host) || {}
164+
newOptions.set(host, { ...currentConfig, packageMirror: newMirror })
165+
return newOptions
166+
})
167+
}
168+
169+
return (
170+
<NativeSelect id={id} name="package-mirror" value={value} onChange={setValue}>
171+
<option value="none">不修改源</option>
172+
<option value="mirrors.aliyun.com">阿里源</option>
173+
<option value="mirrors.tencent.com">腾讯源</option>
174+
<option value="mirrors.tuna.tsinghua.edu.cn">清华源</option>
175+
<option value="mirrors.ustc.edu.cn">中科大源</option>
176+
</NativeSelect>
177+
)
178+
}
179+
180+
function DockerPackageMirrorSelect({ host }: { host: string }) {
181+
const id = useId()
182+
const [options, setOptions] = useAtom(installConfigAtom)
183+
const value = options.get(host)?.dockerPackageMirror || 'mirrors.aliyun.com/docker-ce'
184+
const setValue = (e: React.ChangeEvent<HTMLSelectElement>) => {
185+
const newMirror = e.target.value as InstallOptions['dockerPackageMirror']
186+
setOptions((prev) => {
187+
const newOptions = new Map(prev)
188+
const currentConfig = newOptions.get(host) || {}
189+
newOptions.set(host, { ...currentConfig, dockerPackageMirror: newMirror })
190+
return newOptions
191+
})
192+
}
193+
194+
return (
195+
<NativeSelect id={id} name="docker-package-mirror" value={value} onChange={setValue}>
196+
<option value="mirrors.aliyun.com/docker-ce">阿里源</option>
197+
<option value="mirrors.tencent.com/docker-ce">腾讯源</option>
198+
<option value="mirrors.tuna.tsinghua.edu.cn/docker-ce">清华源</option>
199+
<option value="mirrors.ustc.edu.cn/docker-ce">中科大源</option>
200+
<option value="download.docker.com">官方源</option>
201+
</NativeSelect>
202+
)
203+
}
204+
205+
function NativeSelect({ className, ...props }: ComponentProps<'select'>) {
206+
return (
207+
<select
208+
className={cn(
209+
'shrink-0 rounded-sm border bg-background py-px text-xs shadow-xs outline-none group-data-completed:hidden hover:border-accent-foreground/25 hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[2px] focus-visible:ring-ring/50 disabled:hover:border-border disabled:hover:bg-background disabled:hover:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 dark:disabled:hover:bg-input/30',
210+
className,
211+
)}
212+
{...props}
213+
/>
214+
)
215+
}
216+
148217
function HostConfirmItem({
149218
children,
150219
customIcon,

app/install-env/ssh/ssh-page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414

1515
import * as React from 'react'
1616

17+
import { JotaiProvider } from './jotai-provider'
1718
import { SshHostsConfirm } from './ssh-hosts-confirm'
1819
import { SshInstallPage } from './ssh-install-page'
1920

2021
export function SshPage() {
2122
return (
22-
<>
23+
<JotaiProvider>
2324
<SshHostsConfirm />
2425
<SshInstallPage />
25-
</>
26+
</JotaiProvider>
2627
)
2728
}

lib/ssh/ssh-deployer.ts

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { logger } from '@/lib/logger'
2222
import { messageError, wrapError } from '@/lib/utils/error'
2323
import { SshFinalConnectionInfo } from '@/app/connect-info/schemas'
2424

25-
import { PackageManager } from './pm'
25+
import { DockerPackageMirrors, PackageManager, PackageMirrors } from './pm'
2626
import { MxaCtl, SystemInfo } from './ssh-controller'
2727

2828
const log = logger.child({ module: 'ssh.deployer' })
@@ -53,6 +53,11 @@ export type InstallStepFlags = {
5353
installNvidiaCtk?: InstallFlag
5454
}
5555

56+
export type InstallOptions = {
57+
packageMirror?: PackageMirrors | 'none'
58+
dockerPackageMirror?: DockerPackageMirrors
59+
}
60+
5661
export type SshDeployerStatus = 'idle' | 'failed' | 'installing' | 'completed'
5762

5863
export type SshDeployerInfo = {
@@ -242,14 +247,14 @@ echo "{\
242247
this.ee.on('install:completed', listener)
243248
}
244249

245-
async install({ onProgress }: { onProgress?: LogListener } = {}) {
250+
async install({ onProgress, ...options }: { onProgress?: LogListener } & InstallOptions = {}) {
246251
if (onProgress) {
247252
this.onLog(onProgress)
248253
}
249254
this.beforeInstall()
250-
await this.updateSources()
255+
await this.updateSources(options.packageMirror)
251256
await this.installDependencies()
252-
await this.installDocker()
257+
await this.installDocker(options.dockerPackageMirror)
253258
if (this.installFlags.installNvidiaGpu) {
254259
await this.installNvidiaGpu()
255260
}
@@ -313,20 +318,20 @@ echo "{\
313318
}
314319
}
315320

316-
private async updatePmIndex() {
317-
await this.execInstallScript({
318-
script: this.pm.updateIndex(),
319-
flag: this.installFlags.updateSources,
320-
initLog: '更新软件包索引',
321-
successLog: '软件包索引更新完成',
322-
errorLog: '软件包索引更新失败',
323-
errorMessage: '软件包索引更新失败,请检查网络连接或手动更新软件包索引',
324-
})
325-
}
326-
327-
private async updateSources() {
321+
private async updateSources(mirror?: InstallOptions['packageMirror']) {
322+
if (mirror === 'none') {
323+
await this.execInstallScript({
324+
script: this.pm.updateIndex(),
325+
flag: this.installFlags.updateSources,
326+
initLog: '更新软件包索引',
327+
successLog: '软件包索引更新完成',
328+
errorLog: '软件包索引更新失败',
329+
errorMessage: '软件包索引更新失败,请检查网络连接或手动更新软件包索引',
330+
})
331+
return
332+
}
328333
await this.execInstallScript({
329-
script: this.pm.updateSources(),
334+
script: this.pm.updateSources(mirror),
330335
flag: this.installFlags.updateSources,
331336
initLog: '更新软件源',
332337
successLog: '软件源更新完成',
@@ -353,9 +358,9 @@ echo "{\
353358
})
354359
}
355360

356-
private async installDocker() {
361+
private async installDocker(mirror?: InstallOptions['dockerPackageMirror']) {
357362
await this.execInstallScript({
358-
script: this.pm.installDocker(),
363+
script: this.pm.installDocker(mirror),
359364
flag: this.installFlags.installDocker,
360365
initLog: '安装 Docker',
361366
successLog: 'Docker 安装完成',
@@ -508,17 +513,17 @@ export class SshDeployerManager {
508513
return deployer
509514
}
510515

511-
async installTrigger(host: string) {
516+
async installTrigger(host: string, options: InstallOptions = {}) {
512517
const deployer = this.getDeployer(host)
513-
await deployer.install()
518+
await deployer.install(options)
514519
}
515520

516-
installStream(host: string): EventIterator<string> {
521+
installStream(host: string, options: InstallOptions = {}): EventIterator<string> {
517522
const deployer = this.getDeployer(host)
518523
return new EventIterator<string>(({ push, stop, fail }) => {
519524
deployer.getLogs().forEach((log) => push(log))
520525
deployer
521-
.install({ onProgress: (data) => push(data) })
526+
.install({ onProgress: (data) => push(data), ...options })
522527
.then(() => stop())
523528
.catch((err) => fail(err))
524529
})

trpc/router/ssh-deploy.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import { TRPCError } from '@trpc/server'
1414

1515
import { SshDeployerManager } from '@/lib/ssh/ssh-deployer'
16+
import { z } from '@/lib/zod'
1617
import { sshHostsListSchema } from '@/app/connect-info/schemas'
1718
import { baseProcedure, createRouter } from '@/trpc/init'
1819

@@ -29,6 +30,35 @@ function getSshDeployerManager() {
2930

3031
const installStatusCache = new Map<string, Promise<void>>()
3132

33+
const triggerInputSchema = z.object({
34+
host: z.string(),
35+
options: z
36+
.object({
37+
packageMirror: z
38+
.enum([
39+
'none',
40+
'mirrors.aliyun.com',
41+
'mirrors.tencent.com',
42+
'mirrors.tuna.tsinghua.edu.cn',
43+
'mirrors.ustc.edu.cn',
44+
])
45+
.optional(),
46+
dockerPackageMirror: z
47+
.enum([
48+
'mirrors.aliyun.com/docker-ce',
49+
'mirrors.tencent.com/docker-ce',
50+
'mirrors.tuna.tsinghua.edu.cn/docker-ce',
51+
'mirrors.ustc.edu.cn/docker-ce',
52+
'download.docker.com',
53+
])
54+
.optional(),
55+
})
56+
.default({
57+
packageMirror: 'none',
58+
dockerPackageMirror: 'mirrors.aliyun.com/docker-ce',
59+
}),
60+
})
61+
3262
export const sshDeployRouter = createRouter({
3363
deployer: {
3464
init: baseProcedure.input(sshHostsListSchema).mutation(async ({ input }) => {
@@ -53,13 +83,13 @@ export const sshDeployRouter = createRouter({
5383
}),
5484
},
5585
install: {
56-
trigger: baseProcedure.input(inputType<string>).mutation(async ({ input: host }) => {
57-
const promise = getSshDeployerManager().installTrigger(host)
86+
trigger: baseProcedure.input(triggerInputSchema).mutation(async ({ input: { host, options } }) => {
87+
const promise = getSshDeployerManager().installTrigger(host, options)
5888
installStatusCache.set(host, promise)
5989
return await promise
6090
}),
61-
triggerOnce: baseProcedure.input(inputType<string>).mutation(async ({ input: host }) => {
62-
const promise = getSshDeployerManager().installTrigger(host)
91+
triggerOnce: baseProcedure.input(triggerInputSchema).mutation(async ({ input: { host, options } }) => {
92+
const promise = getSshDeployerManager().installTrigger(host, options)
6393
installStatusCache.set(host, promise)
6494
}),
6595
status: baseProcedure.input(inputType<string>).query(async ({ input: host }) => {

0 commit comments

Comments
 (0)