feat(header): add self check error banner

This commit is contained in:
Jacky 2025-04-23 14:48:39 +00:00
parent 309ca81ee3
commit 597175940f
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
8 changed files with 187 additions and 55 deletions

1
app/components.d.ts vendored
View file

@ -107,6 +107,7 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SelfCheckSelfCheck: typeof import('./src/components/SelfCheck/SelfCheck.vue')['default']
SelfCheckSelfCheckHeaderBanner: typeof import('./src/components/SelfCheck/SelfCheckHeaderBanner.vue')['default']
SensitiveStringSensitiveString: typeof import('./src/components/SensitiveString/SensitiveString.vue')['default']
SetLanguageSetLanguage: typeof import('./src/components/SetLanguage/SetLanguage.vue')['default']
StdDesignStdDataDisplayStdBatchEdit: typeof import('./src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue')['default']

View file

@ -1,48 +1,14 @@
<script setup lang="ts">
import type { TaskReport } from './tasks'
import { CheckCircleOutlined, CloseCircleOutlined, WarningOutlined } from '@ant-design/icons-vue'
import { useSelfCheckStore } from './store'
import { taskManager } from './tasks'
const data = ref<TaskReport[]>()
const requestError = ref(false)
const loading = ref(false)
const store = useSelfCheckStore()
async function check() {
loading.value = true
try {
data.value = await taskManager.runAllChecks()
}
catch {
requestError.value = true
}
finally {
loading.value = false
}
}
const { data, loading, fixing } = storeToRefs(store)
onMounted(() => {
check()
})
const fixing = reactive({})
async function fix(taskName: string) {
fixing[taskName] = true
try {
await taskManager.fixTask(taskName)
check()
}
finally {
fixing[taskName] = false
}
}
const hasError = computed(() => {
return requestError.value || data.value?.some(item => item.status === 'error')
})
defineExpose({
hasError,
store.check()
})
</script>
@ -50,7 +16,10 @@ defineExpose({
<ACard :title="$gettext('Self Check')">
<template #extra>
<AButton
type="link" size="small" :loading="loading" @click="check"
type="link"
size="small"
:loading="loading"
@click="store.check"
>
{{ $gettext('Recheck') }}
</AButton>
@ -58,7 +27,7 @@ defineExpose({
<AList>
<AListItem v-for="(item, index) in data" :key="index">
<template v-if="item.status === 'error'" #actions>
<AButton type="link" size="small" :loading="fixing[item.name]" @click="fix(item.name)">
<AButton type="link" size="small" :loading="fixing[item.name]" @click="store.fix(item.name)">
{{ $gettext('Attempt to fix') }}
</AButton>
</template>

View file

@ -0,0 +1,88 @@
<script setup lang="ts">
import { CloseCircleOutlined } from '@ant-design/icons-vue'
import { useElementSize } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { useSelfCheckStore } from './store'
const props = defineProps<{
headerWeight?: number
userWrapperWidth?: number
}>()
const router = useRouter()
const selfCheckStore = useSelfCheckStore()
const { hasError } = storeToRefs(selfCheckStore)
const alertEl = useTemplateRef('alertEl')
const { width: alertWidth } = useElementSize(alertEl)
const shouldHideAlert = computed(() => {
if (!props.headerWeight || !props.userWrapperWidth || !alertWidth.value)
return false
return (props.headerWeight - props.userWrapperWidth - alertWidth.value - 60) < props.userWrapperWidth
})
const iconRightPosition = computed(() => {
return props.userWrapperWidth ? `${props.userWrapperWidth + 50}px` : '50px'
})
onMounted(() => {
selfCheckStore.check()
})
</script>
<template>
<div v-show="hasError">
<div ref="alertEl" class="self-check-alert" :style="{ visibility: shouldHideAlert ? 'hidden' : 'visible' }">
<AAlert type="error" show-icon :message="$gettext('Self check failed, Nginx UI may not work properly')">
<template #action>
<AButton class="ml-4" size="small" danger @click="router.push('/system/self_check')">
{{ $gettext('Check') }}
</AButton>
</template>
</AAlert>
</div>
<APopover
v-if="shouldHideAlert"
placement="bottomRight"
trigger="hover"
>
<CloseCircleOutlined
class="error-icon"
:style="{ right: iconRightPosition }"
@click="router.push('/system/self_check')"
/>
<template #content>
<div class="flex items-center gap-2">
<CloseCircleOutlined class="text-red-500" />
<div>
{{ $gettext('Self check failed, Nginx UI may not work properly') }}
</div>
<div>
<AButton size="small" danger @click="router.push('/system/self_check')">
{{ $gettext('Check') }}
</AButton>
</div>
</div>
</template>
</APopover>
</div>
</template>
<style lang="less" scoped>
.self-check-alert {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.error-icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
color: #f5222d;
cursor: pointer;
}
</style>

View file

@ -1,3 +1,10 @@
import SelfCheck from './SelfCheck.vue'
import SelfCheckHeaderBanner from './SelfCheckHeaderBanner.vue'
import { useSelfCheckStore } from './store'
export {
SelfCheckHeaderBanner,
useSelfCheckStore,
}
export default SelfCheck

View file

@ -0,0 +1,54 @@
import type { TaskReport } from './tasks'
import { debounce } from 'lodash'
import { taskManager } from './tasks'
export const useSelfCheckStore = defineStore('selfCheck', () => {
const data = ref<TaskReport[]>([])
const requestError = ref(false)
const loading = ref(false)
async function __check() {
if (loading.value)
return
loading.value = true
try {
data.value = await taskManager.runAllChecks()
}
catch (error) {
console.error(error)
requestError.value = true
}
finally {
loading.value = false
}
}
const check = debounce(__check, 1000, {
leading: true,
trailing: false,
})
const fixing = reactive({})
async function fix(taskName: string) {
if (fixing[taskName])
return
fixing[taskName] = true
try {
await taskManager.fixTask(taskName)
check()
}
finally {
fixing[taskName] = false
}
}
const hasError = computed(() => {
return requestError.value || data.value?.some(item => item.status === 'error')
})
return { data, loading, fixing, hasError, check, fix }
})

View file

@ -1,11 +1,12 @@
<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'
import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
import SwitchAppearance from '@/components/SwitchAppearance/SwitchAppearance.vue'
import NginxControl from '@/components/NginxControl'
import Notification from '@/components/Notification'
import { SelfCheckHeaderBanner } from '@/components/SelfCheck'
import SetLanguage from '@/components/SetLanguage'
import SwitchAppearance from '@/components/SwitchAppearance'
import { DesktopOutlined, HomeOutlined, LogoutOutlined, MenuUnfoldOutlined } from '@ant-design/icons-vue'
import { useElementSize } from '@vueuse/core'
import { message } from 'ant-design-vue'
import { useRouter } from 'vue-router'
@ -23,11 +24,16 @@ function logout() {
})
}
const headerRef = useTemplateRef('headerRef') as Readonly<ShallowRef<HTMLDivElement>>
const headerRef = useTemplateRef('headerRef')
const userWrapperRef = useTemplateRef('userWrapperRef')
const isWorkspace = computed(() => {
return !!window.inWorkspace
})
const { width: headerWidth } = useElementSize(headerRef)
const { width: userWrapperWidth } = useElementSize(userWrapperRef)
</script>
<template>
@ -36,7 +42,13 @@ const isWorkspace = computed(() => {
<MenuUnfoldOutlined @click="emit('clickUnFold')" />
</div>
<SelfCheckHeaderBanner
:header-weight="headerWidth"
:user-wrapper-width="userWrapperWidth"
/>
<ASpace
ref="userWrapperRef"
class="user-wrapper"
:size="24"
>
@ -74,7 +86,7 @@ const isWorkspace = computed(() => {
background: transparent;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.05);
width: 100%;
position: relative;
a {
color: #000000;
}

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import install from '@/api/install'
import SelfCheck from '@/components/SelfCheck'
import SelfCheck, { useSelfCheckStore } from '@/components/SelfCheck'
import SystemRestoreContent from '@/components/SystemRestore'
import { message } from 'ant-design-vue'
import InstallFooter from './InstallFooter.vue'
@ -11,7 +11,8 @@ import TimeoutAlert from './TimeoutAlert.vue'
const installTimeout = ref(false)
const activeTab = ref('1')
const step = ref(1)
const selfCheckRef = useTemplateRef('selfCheckRef')
const selfCheckStore = useSelfCheckStore()
const { hasError } = storeToRefs(selfCheckStore)
const router = useRouter()
@ -50,7 +51,7 @@ function handleRestoreSuccess(options: { restoreNginx: boolean, restoreNginxUI:
}
const canProceed = computed(() => {
return !installTimeout.value && !selfCheckRef.value?.hasError
return !installTimeout.value && !hasError.value
})
const steps = [
@ -87,7 +88,7 @@ const steps = [
</ASteps>
<div v-if="step === 1">
<SelfCheck ref="selfCheckRef" class="mb-4" />
<SelfCheck class="mb-4" />
<div class="flex justify-center">
<AButton v-if="canProceed" type="primary" @click="step = 2">
{{ $gettext('Next') }}

View file

@ -18,15 +18,15 @@ func getToken(c *gin.Context) (token string) {
return
}
if token, _ = c.Cookie("token"); token != "" {
return token
}
if token = c.Query("token"); token != "" {
tokenBytes, _ := base64.StdEncoding.DecodeString(token)
return string(tokenBytes)
}
if token, _ = c.Cookie("token"); token != "" {
return token
}
return ""
}