mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 11:46:36 +08:00
fix library display and docker compose update
This commit is contained in:
@@ -4,6 +4,10 @@ import type { AccessLog, Setting, User } from '../types'
|
||||
export interface SystemUpdateStatus {
|
||||
image: string
|
||||
watchtower_image?: string
|
||||
update_mode?: string
|
||||
compose_dir?: string
|
||||
compose_file?: string
|
||||
compose_command?: string
|
||||
container_id?: string
|
||||
container_name?: string
|
||||
current_image_id?: string
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ChevronDown, LogOut, Settings, User as UserIcon, UserCog } from 'lucide-react'
|
||||
import { ChevronDown, Loader2, LogOut, RotateCw, Settings, User as UserIcon, UserCog } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { adminAPI } from '../api/admin'
|
||||
import type { PlayProfile } from '../types'
|
||||
|
||||
type LayoutUser = {
|
||||
@@ -39,6 +41,7 @@ export function LayoutUserMenu({
|
||||
const location = useLocation()
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
const lastLocationRef = useRef(`${location.pathname}${location.search}`)
|
||||
const [updating, setUpdating] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return undefined
|
||||
@@ -68,6 +71,22 @@ export function LayoutUserMenu({
|
||||
if (isOpen) onClose()
|
||||
}, [isOpen, location.pathname, location.search, onClose])
|
||||
|
||||
const applySystemUpdate = async () => {
|
||||
if (updating) return
|
||||
setUpdating(true)
|
||||
try {
|
||||
const status = await adminAPI.systemUpdateApply()
|
||||
toast.success(status.message || '系统更新任务已启动')
|
||||
onClose()
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
(err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '启动系统更新失败'
|
||||
toast.error(msg)
|
||||
} finally {
|
||||
setUpdating(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className="relative" data-testid="layout-user-menu">
|
||||
<button
|
||||
@@ -102,6 +121,17 @@ export function LayoutUserMenu({
|
||||
{user?.role === 'admin' && (
|
||||
<UserMenuLink to="/admin" icon={<Settings size={16} />} label="管理主控制台" onClick={onClose} />
|
||||
)}
|
||||
{user?.role === 'admin' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={applySystemUpdate}
|
||||
disabled={updating}
|
||||
className="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-sm text-[var(--app-subtle)] transition-colors hover:bg-[var(--app-hover)] hover:text-[var(--app-text)] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{updating ? <Loader2 size={16} className="animate-spin" /> : <RotateCw size={16} />}
|
||||
<span>一键更新系统</span>
|
||||
</button>
|
||||
)}
|
||||
<div className="my-1.5 border-t border-[var(--app-border)]" />
|
||||
<div className="px-3 py-2">
|
||||
<p className="mb-2 text-[10px] font-bold uppercase tracking-wider text-[var(--app-muted)]">
|
||||
|
||||
@@ -100,7 +100,7 @@ export function SystemUpdatePanel() {
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h2 className="font-display text-xl font-semibold text-ink-600">Docker 热更新</h2>
|
||||
<h2 className="font-display text-xl font-semibold text-ink-600">Docker Compose 一键更新</h2>
|
||||
<span className={`inline-flex items-center gap-1 rounded-lg border px-2 py-1 text-xs ${tone.className}`}>
|
||||
<ToneIcon size={13} />
|
||||
{tone.label}
|
||||
@@ -130,7 +130,7 @@ export function SystemUpdatePanel() {
|
||||
<button
|
||||
type="button"
|
||||
className="btn-primary"
|
||||
disabled={applying || status?.running || !status?.can_apply}
|
||||
disabled={applying || status?.running}
|
||||
onClick={applyUpdate}
|
||||
>
|
||||
{applying || status?.running ? <Loader2 size={16} className="animate-spin" /> : <RotateCw size={16} />}
|
||||
@@ -140,13 +140,15 @@ export function SystemUpdatePanel() {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<UpdateFact icon={Server} label="Docker" value={status?.docker_available ? '可连接' : '未就绪'} />
|
||||
<UpdateFact icon={Server} label="更新方式" value={status?.update_mode === 'compose' ? 'Docker Compose' : '自定义命令'} />
|
||||
<UpdateFact label="Compose 目录" value={status?.compose_dir || '自动识别'} mono />
|
||||
<UpdateFact label="应用镜像" value={status?.image || '-'} mono />
|
||||
<UpdateFact label="当前容器" value={status?.container_name || status?.container_id || '-'} mono />
|
||||
<UpdateFact label="最近检查" value={formatDate(status?.checked_at)} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<DigestRow label="Compose 文件" digest={status?.compose_file} />
|
||||
<DigestRow label="Docker 连接" digest={status?.docker_available ? '可连接' : '未连接或未检查'} />
|
||||
<DigestRow label="本地摘要" digest={status?.local_digest} />
|
||||
<DigestRow label="远端摘要" digest={status?.remote_digest} />
|
||||
</div>
|
||||
@@ -217,7 +219,7 @@ function updateTone(status: SystemUpdateStatus | null): {
|
||||
if (status.running) {
|
||||
return { label: '更新中', className: 'border-yellow-400/50 text-yellow-600', icon: Loader2 }
|
||||
}
|
||||
if (!status.docker_available || !status.can_apply) {
|
||||
if (!status.can_apply && status.message && status.message !== '尚未检查更新') {
|
||||
return { label: '不可更新', className: 'border-red-400/50 text-red-500', icon: AlertTriangle }
|
||||
}
|
||||
if (status.update_available === true) {
|
||||
@@ -234,13 +236,6 @@ function shortDigest(digest?: string): string {
|
||||
return digest.length > 24 ? `${digest.slice(0, 18)}…${digest.slice(-8)}` : digest
|
||||
}
|
||||
|
||||
function formatDate(value?: string): string {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
function apiErrorMessage(err: unknown, fallback: string): string {
|
||||
return (err as UpdateErrorPayload).response?.data?.error || fallback
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { SettingGroup } from './settingsGroupTypes'
|
||||
export const systemUpdateSettingsGroup: SettingGroup = {
|
||||
key: 'system-update',
|
||||
label: '系统更新',
|
||||
description: 'Docker 部署可在这里检查并拉取最新版镜像。',
|
||||
description: 'Docker Compose 部署可在这里检查并拉取最新版镜像。',
|
||||
items: [
|
||||
{
|
||||
key: 'system.update.image',
|
||||
@@ -14,20 +14,19 @@ export const systemUpdateSettingsGroup: SettingGroup = {
|
||||
hint: '用于检查远端摘要;保持 latest 即可跟随主分支镜像。',
|
||||
},
|
||||
{
|
||||
key: 'system.update.watchtower_image',
|
||||
label: 'Watchtower 镜像',
|
||||
key: 'system.update.compose_dir',
|
||||
label: 'Docker Compose 安装目录',
|
||||
type: 'text',
|
||||
defaultValue: 'containrrr/watchtower:latest',
|
||||
placeholder: 'containrrr/watchtower:latest',
|
||||
hint: '默认使用一次性 Watchtower 更新当前容器。',
|
||||
placeholder: '/vol1/1000/docker/mediastation-go',
|
||||
hint: '留空时自动查找 docker-compose.yml / compose.yml 所在目录。',
|
||||
},
|
||||
{
|
||||
key: 'system.update.command',
|
||||
label: '自定义更新命令',
|
||||
type: 'textarea',
|
||||
placeholder:
|
||||
'docker run --rm -v /var/run/docker.sock:/var/run/docker.sock {{watchtower_image}} --run-once --cleanup {{container}}',
|
||||
hint: '留空时使用默认命令。支持 {{image}}、{{watchtower_image}}、{{container}}、{{container_id}}、{{container_name}}。',
|
||||
'cd {{compose_dir}} && {{compose_command}} pull && {{compose_command}} up -d && docker image prune -f && docker restart {{container}}',
|
||||
hint: '留空时使用 Docker Compose 默认命令。支持 {{image}}、{{compose_dir}}、{{compose_file}}、{{compose_command}}、{{container}}、{{container_id}}、{{container_name}}。',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user