Add UV mirrors settings (#2333)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Chenlei Hu
2025-01-28 14:39:51 -08:00
committed by GitHub
parent 29b5f606b0
commit 430f051c64
22 changed files with 392 additions and 32 deletions

View File

@@ -4,18 +4,18 @@
v-bind="$attrs"
:model-value="internalValue"
class="w-full"
:invalid="validationState === UrlValidationState.INVALID"
:invalid="validationState === ValidationState.INVALID"
@update:model-value="handleInput"
@blur="handleBlur"
/>
<InputIcon
:class="{
'pi pi-spin pi-spinner text-neutral-400':
validationState === UrlValidationState.LOADING,
validationState === ValidationState.LOADING,
'pi pi-check text-green-500 cursor-pointer':
validationState === UrlValidationState.VALID,
validationState === ValidationState.VALID,
'pi pi-times text-red-500 cursor-pointer':
validationState === UrlValidationState.INVALID
validationState === ValidationState.INVALID
}"
@click="validateUrl(props.modelValue)"
/>
@@ -30,6 +30,7 @@ import { onMounted, ref, watch } from 'vue'
import { isValidUrl } from '@/utils/formatUtil'
import { checkUrlReachable } from '@/utils/networkUtil'
import { ValidationState } from '@/utils/validationUtil'
const props = defineProps<{
modelValue: string
@@ -38,16 +39,10 @@ const props = defineProps<{
const emit = defineEmits<{
'update:modelValue': [value: string]
'state-change': [state: ValidationState]
}>()
enum UrlValidationState {
IDLE = 'IDLE',
LOADING = 'LOADING',
VALID = 'VALID',
INVALID = 'INVALID'
}
const validationState = ref<UrlValidationState>(UrlValidationState.IDLE)
const validationState = ref<ValidationState>(ValidationState.IDLE)
// Add internal value state
const internalValue = ref(props.modelValue)
@@ -60,6 +55,11 @@ watch(
await validateUrl(newValue)
}
)
watch(validationState, (newState) => {
emit('state-change', newState)
})
// Validate on mount
onMounted(async () => {
await validateUrl(props.modelValue)
@@ -69,7 +69,7 @@ const handleInput = (value: string) => {
// Update internal value without emitting
internalValue.value = value
// Reset validation state when user types
validationState.value = UrlValidationState.IDLE
validationState.value = ValidationState.IDLE
}
const handleBlur = async () => {
@@ -88,24 +88,24 @@ const defaultValidateUrl = async (url: string): Promise<boolean> => {
}
const validateUrl = async (value: string) => {
if (validationState.value === UrlValidationState.LOADING) return
if (validationState.value === ValidationState.LOADING) return
const url = value.trim()
// Reset state
validationState.value = UrlValidationState.IDLE
validationState.value = ValidationState.IDLE
// Skip validation if empty
if (!url) return
validationState.value = UrlValidationState.LOADING
validationState.value = ValidationState.LOADING
try {
const isValid = await (props.validateUrlFn ?? defaultValidateUrl)(url)
validationState.value = isValid
? UrlValidationState.VALID
: UrlValidationState.INVALID
? ValidationState.VALID
: ValidationState.INVALID
} catch {
validationState.value = UrlValidationState.INVALID
validationState.value = ValidationState.INVALID
}
}

View File

@@ -0,0 +1,70 @@
<template>
<Panel
:header="$t('install.settings.mirrorSettings')"
toggleable
:collapsed="!showMirrorInputs"
pt:root="bg-neutral-800 border-none w-[600px]"
>
<template
v-for="([item, modelValue], index) in mirrors"
:key="item.settingId"
>
<Divider v-if="index > 0" />
<MirrorItem
:item="item"
v-model="modelValue.value"
@state-change="validationStates[index] = $event"
/>
</template>
<template #icons>
<i
:class="{
'pi pi-spin pi-spinner text-neutral-400':
validationState === ValidationState.LOADING,
'pi pi-check text-green-500':
validationState === ValidationState.VALID,
'pi pi-times text-red-500':
validationState === ValidationState.INVALID
}"
v-tooltip="validationStateTooltip"
/>
</template>
</Panel>
</template>
<script setup lang="ts">
import _ from 'lodash'
import Divider from 'primevue/divider'
import Panel from 'primevue/panel'
import { computed, ref } from 'vue'
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
import { UV_MIRRORS } from '@/constants/uvMirrors'
import { t } from '@/i18n'
import { ValidationState, mergeValidationStates } from '@/utils/validationUtil'
const showMirrorInputs = ref(false)
const pythonMirror = defineModel<string>('pythonMirror', { required: true })
const pypiMirror = defineModel<string>('pypiMirror', { required: true })
const torchMirror = defineModel<string>('torchMirror', { required: true })
const mirrors = _.zip(UV_MIRRORS, [pythonMirror, pypiMirror, torchMirror])
const validationStates = ref<ValidationState[]>(
mirrors.map(() => ValidationState.IDLE)
)
const validationState = computed(() => {
return mergeValidationStates(validationStates.value)
})
const validationStateTooltip = computed(() => {
switch (validationState.value) {
case ValidationState.INVALID:
return t('install.settings.mirrorsUnreachable')
case ValidationState.VALID:
return t('install.settings.mirrorsReachable')
default:
return t('install.settings.checkingMirrors')
}
})
</script>

View File

@@ -0,0 +1,66 @@
<template>
<div class="flex flex-col items-center gap-4">
<div class="w-full">
<h3 class="text-lg font-medium text-neutral-100">
{{ $t(`settings.${normalizedSettingId}.name`) }}
</h3>
<p class="text-sm text-neutral-400 mt-1">
{{ $t(`settings.${normalizedSettingId}.tooltip`) }}
</p>
</div>
<UrlInput
v-model="modelValue"
:validate-url-fn="checkMirrorReachable"
@state-change="validationState = $event"
/>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { UVMirror } from '@/constants/uvMirrors'
import { electronAPI } from '@/utils/envUtil'
import { isValidUrl, normalizeI18nKey } from '@/utils/formatUtil'
import { ValidationState } from '@/utils/validationUtil'
const { item } = defineProps<{
item: UVMirror
}>()
const emit = defineEmits<{
'state-change': [state: ValidationState]
}>()
const modelValue = defineModel<string>('modelValue', { required: true })
const validationState = ref<ValidationState>(ValidationState.IDLE)
const normalizedSettingId = computed(() => {
return normalizeI18nKey(item.settingId)
})
const checkMirrorReachable = async (mirror: string) => {
return (
isValidUrl(mirror) &&
(await electronAPI().NetWork.canAccessUrl(
mirror + (item.validationPathSuffix ?? '')
))
)
}
onMounted(() => {
modelValue.value = item.mirror
})
watch(validationState, (newState) => {
emit('state-change', newState)
// Set fallback mirror if default mirror is invalid
if (
newState === ValidationState.INVALID &&
modelValue.value === item.mirror
) {
modelValue.value = item.fallbackMirror
}
})
</script>