[codex] tighten update center and proxy debug UX (#325)
* tighten update center and proxy debug UX * Address update center review feedback
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
export type UpdateCenterHistoryEntry = {
|
||||
revision?: string;
|
||||
updatedAt?: string | null;
|
||||
status?: string | null;
|
||||
description?: string | null;
|
||||
imageTag?: string | null;
|
||||
imageDigest?: string | null;
|
||||
};
|
||||
|
||||
type UpdateCenterHistoryEntryCardProps = {
|
||||
entry: UpdateCenterHistoryEntry;
|
||||
currentRevision: string;
|
||||
helperHealthy: boolean;
|
||||
deploying: boolean;
|
||||
compact?: boolean;
|
||||
formatTaskTime: (value?: string | null) => string;
|
||||
formatImageTarget: (tag?: string | null, digest?: string | null) => string;
|
||||
onRollback: (revision: string) => void;
|
||||
};
|
||||
|
||||
export default function UpdateCenterHistoryEntryCard({
|
||||
entry,
|
||||
currentRevision,
|
||||
helperHealthy,
|
||||
deploying,
|
||||
compact = false,
|
||||
formatTaskTime,
|
||||
formatImageTarget,
|
||||
onRollback,
|
||||
}: UpdateCenterHistoryEntryCardProps) {
|
||||
const revision = String(entry?.revision || '').trim();
|
||||
const isCurrentRevision = revision && revision === currentRevision;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--color-border-light)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
padding: compact ? 10 : 12,
|
||||
display: 'grid',
|
||||
gap: compact ? 5 : 6,
|
||||
background: isCurrentRevision
|
||||
? 'color-mix(in srgb, var(--color-primary) 6%, var(--color-bg-card))'
|
||||
: 'var(--color-bg-card)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text-primary)' }}>
|
||||
revision {revision || '-'}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
{entry?.status ? <span className="badge badge-muted">{entry.status}</span> : null}
|
||||
{isCurrentRevision ? <span className="badge badge-info">当前运行</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text-primary)', fontFamily: 'var(--font-mono)' }}>
|
||||
{formatImageTarget(entry?.imageTag, entry?.imageDigest) || '未记录镜像信息'}
|
||||
</div>
|
||||
{entry?.description ? (
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.5 }}>
|
||||
{entry.description}
|
||||
</div>
|
||||
) : null}
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.5 }}>
|
||||
更新时间:{formatTaskTime(entry?.updatedAt)}
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isCurrentRevision) return;
|
||||
onRollback(revision);
|
||||
}}
|
||||
disabled={!helperHealthy || deploying || isCurrentRevision || !revision}
|
||||
className="btn btn-ghost"
|
||||
style={{ border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
回退到 revision {revision}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import CenteredModal from '../../components/CenteredModal.js';
|
||||
import UpdateCenterHistoryEntryCard, { type UpdateCenterHistoryEntry } from './UpdateCenterHistoryEntryCard.js';
|
||||
|
||||
type UpdateCenterHistoryModalProps = {
|
||||
open: boolean;
|
||||
helperHealthy: boolean;
|
||||
deploying: boolean;
|
||||
currentRevision: string;
|
||||
history: UpdateCenterHistoryEntry[];
|
||||
formatTaskTime: (value?: string | null) => string;
|
||||
formatImageTarget: (tag?: string | null, digest?: string | null) => string;
|
||||
onClose: () => void;
|
||||
onRollback: (revision: string) => void;
|
||||
};
|
||||
|
||||
export default function UpdateCenterHistoryModal({
|
||||
open,
|
||||
helperHealthy,
|
||||
deploying,
|
||||
currentRevision,
|
||||
history,
|
||||
formatTaskTime,
|
||||
formatImageTarget,
|
||||
onClose,
|
||||
onRollback,
|
||||
}: UpdateCenterHistoryModalProps) {
|
||||
return (
|
||||
<CenteredModal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title="全部 revision"
|
||||
maxWidth={880}
|
||||
closeOnBackdrop
|
||||
closeOnEscape
|
||||
footer={(
|
||||
<button type="button" className="btn btn-ghost" onClick={onClose}>
|
||||
关闭
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
<div style={{ display: 'grid', gap: 10 }}>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.6 }}>
|
||||
这里保留 helper 读到的全部 Helm revision。默认列表只显示最近几条,弹窗里再展开全部回退记录,避免设置页被历史卡片拉得过长。
|
||||
</div>
|
||||
<div style={{ display: 'grid', gap: 10, maxHeight: 520, overflowY: 'auto', paddingRight: 4 }}>
|
||||
{history.map((entry) => {
|
||||
const revision = String(entry?.revision || '').trim();
|
||||
return (
|
||||
<UpdateCenterHistoryEntryCard
|
||||
key={revision || 'unknown-revision-modal'}
|
||||
entry={entry}
|
||||
currentRevision={currentRevision}
|
||||
helperHealthy={helperHealthy}
|
||||
deploying={deploying}
|
||||
formatTaskTime={formatTaskTime}
|
||||
formatImageTarget={formatImageTarget}
|
||||
onRollback={onRollback}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</CenteredModal>
|
||||
);
|
||||
}
|
||||
@@ -40,6 +40,12 @@ describe('UpdateCenterSection', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
globalThis.document = {
|
||||
body: {
|
||||
nodeType: 1,
|
||||
style: {
|
||||
overflow: '',
|
||||
},
|
||||
},
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
} as unknown as Document;
|
||||
@@ -70,9 +76,19 @@ describe('UpdateCenterSection', () => {
|
||||
helper: {
|
||||
ok: true,
|
||||
healthy: true,
|
||||
revision: '17',
|
||||
imageTag: 'latest',
|
||||
imageDigest: 'sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
|
||||
history: [
|
||||
{
|
||||
revision: '17',
|
||||
updatedAt: '2026-03-29T12:00:00Z',
|
||||
status: 'deployed',
|
||||
description: 'Upgrade complete',
|
||||
imageRepository: '1467078763/metapi',
|
||||
imageTag: 'latest',
|
||||
imageDigest: 'sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
|
||||
},
|
||||
{
|
||||
revision: '16',
|
||||
updatedAt: '2026-03-28T12:00:00Z',
|
||||
@@ -82,6 +98,15 @@ describe('UpdateCenterSection', () => {
|
||||
imageTag: 'main',
|
||||
imageDigest: 'sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
},
|
||||
{
|
||||
revision: '15',
|
||||
updatedAt: '2026-03-27T12:00:00Z',
|
||||
status: 'superseded',
|
||||
description: 'Earlier stable release',
|
||||
imageRepository: '1467078763/metapi',
|
||||
imageTag: '1.2.2',
|
||||
imageDigest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
},
|
||||
],
|
||||
},
|
||||
runningTask: null,
|
||||
@@ -228,6 +253,7 @@ describe('UpdateCenterSection', () => {
|
||||
|
||||
const text = collectText(root.root);
|
||||
expect(text).toContain('latest @ sha256:efb2ee655386');
|
||||
expect(text).toContain('发现新版本');
|
||||
expect(text).toContain('Running helm upgrade');
|
||||
expect(text).toContain('Waiting for rollout');
|
||||
expect(text).toContain('任务状态 · 已完成');
|
||||
@@ -306,6 +332,111 @@ describe('UpdateCenterSection', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps rollback history compact and opens the full revision list in a centered modal', async () => {
|
||||
let root!: ReactTestRenderer;
|
||||
try {
|
||||
await act(async () => {
|
||||
root = create(
|
||||
<MemoryRouter>
|
||||
<ToastProvider>
|
||||
<UpdateCenterSection />
|
||||
</ToastProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
const compactText = collectText(root.root);
|
||||
expect(compactText).toContain('展开全部 3 条');
|
||||
expect(compactText).not.toContain('Earlier stable release');
|
||||
expect(compactText).not.toContain('回退到 revision 15');
|
||||
|
||||
const openHistoryButton = root.root.find((node) => (
|
||||
node.type === 'button'
|
||||
&& typeof node.props.onClick === 'function'
|
||||
&& collectText(node).includes('展开全部 3 条')
|
||||
));
|
||||
|
||||
await act(async () => {
|
||||
await openHistoryButton.props.onClick();
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
const modal = root.root.find((node) => (
|
||||
typeof node.props.className === 'string'
|
||||
&& node.props.className.includes('modal-content')
|
||||
&& collectText(node).includes('全部 revision')
|
||||
));
|
||||
|
||||
expect(collectText(modal)).toContain('Earlier stable release');
|
||||
expect(collectText(modal)).toContain('回退到 revision 15');
|
||||
} finally {
|
||||
root?.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it('blocks Docker Hub deploys when the current helper image already matches the target digest', async () => {
|
||||
apiMock.getUpdateCenterStatus.mockResolvedValueOnce({
|
||||
currentVersion: '1.2.3',
|
||||
config: {
|
||||
enabled: true,
|
||||
helperBaseUrl: 'http://metapi-deploy-helper.ai.svc.cluster.local:9850',
|
||||
namespace: 'ai',
|
||||
releaseName: 'metapi',
|
||||
chartRef: 'oci://ghcr.io/cita-777/charts/metapi',
|
||||
imageRepository: '1467078763/metapi',
|
||||
githubReleasesEnabled: true,
|
||||
dockerHubTagsEnabled: true,
|
||||
defaultDeploySource: 'docker-hub-tag',
|
||||
},
|
||||
githubRelease: {
|
||||
normalizedVersion: '1.3.0',
|
||||
displayVersion: '1.3.0',
|
||||
},
|
||||
dockerHubTag: {
|
||||
normalizedVersion: 'latest',
|
||||
tagName: 'latest',
|
||||
digest: 'sha256:efb2ee6553866bd3268dcc54c02fa5f9789728c51ed4af63328aaba6da67df35',
|
||||
displayVersion: 'latest @ sha256:efb2ee655386',
|
||||
},
|
||||
helper: {
|
||||
ok: true,
|
||||
healthy: true,
|
||||
revision: '17',
|
||||
imageTag: 'latest',
|
||||
imageDigest: 'sha256:efb2ee6553866bd3268dcc54c02fa5f9789728c51ed4af63328aaba6da67df35',
|
||||
},
|
||||
runningTask: null,
|
||||
lastFinishedTask: null,
|
||||
});
|
||||
|
||||
let root!: ReactTestRenderer;
|
||||
try {
|
||||
await act(async () => {
|
||||
root = create(
|
||||
<MemoryRouter>
|
||||
<ToastProvider>
|
||||
<UpdateCenterSection />
|
||||
</ToastProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
const dockerDeployButton = root.root.find((node) => (
|
||||
node.type === 'button'
|
||||
&& typeof node.props.className === 'string'
|
||||
&& node.props.className.includes('btn')
|
||||
&& collectText(node).includes('部署 Docker Hub 标签')
|
||||
));
|
||||
|
||||
expect(dockerDeployButton.props.disabled).toBe(true);
|
||||
expect(collectText(root.root)).toContain('当前已运行该镜像');
|
||||
} finally {
|
||||
root?.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it('renders rollback history and triggers rollback tasks for previous revisions', async () => {
|
||||
let root!: ReactTestRenderer;
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,13 @@ import { api } from '../../api.js';
|
||||
import ModernSelect from '../../components/ModernSelect.js';
|
||||
import { useToast } from '../../components/Toast.js';
|
||||
import { useIsMobile } from '../../components/useIsMobile.js';
|
||||
import {
|
||||
buildUpdateReminder,
|
||||
describeDockerDeployState,
|
||||
describeGitHubDeployState,
|
||||
} from '../helpers/updateCenterPresentation.js';
|
||||
import UpdateCenterHistoryModal from './UpdateCenterHistoryModal.js';
|
||||
import UpdateCenterHistoryEntryCard from './UpdateCenterHistoryEntryCard.js';
|
||||
|
||||
type UpdateCenterStatus = {
|
||||
currentVersion?: string;
|
||||
@@ -206,6 +213,7 @@ export default function UpdateCenterSection() {
|
||||
const [config, setConfig] = useState(DEFAULT_CONFIG);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [taskStatus, setTaskStatus] = useState('');
|
||||
const [historyModalOpen, setHistoryModalOpen] = useState(false);
|
||||
const streamAbortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const loadStatus = async () => {
|
||||
@@ -367,29 +375,33 @@ export default function UpdateCenterSection() {
|
||||
const runningTaskBadge = getTaskBadge(status?.runningTask?.status || taskStatus || undefined);
|
||||
const lastFinishedTaskBadge = getTaskBadge(status?.lastFinishedTask?.status || undefined);
|
||||
const visibleTaskStatus = taskStatus || status?.runningTask?.status || status?.lastFinishedTask?.status || 'idle';
|
||||
|
||||
const canDeployGithub = Boolean(
|
||||
!deploying
|
||||
&& config.enabled
|
||||
&& helperHealthy
|
||||
&& config.githubReleasesEnabled
|
||||
&& status?.githubRelease?.normalizedVersion,
|
||||
);
|
||||
const canDeployDocker = Boolean(
|
||||
!deploying
|
||||
&& config.enabled
|
||||
&& helperHealthy
|
||||
&& config.dockerHubTagsEnabled
|
||||
&& status?.dockerHubTag?.normalizedVersion,
|
||||
);
|
||||
|
||||
const renderDeployReason = (enabled: boolean, version?: string, helperError?: string | null) => {
|
||||
if (!config.enabled) return '更新中心已关闭,保存启用后才能部署。';
|
||||
if (!enabled) return '当前来源已停用,开启后才会参与检查和部署。';
|
||||
if (!helperHealthy) return helperError || 'Deploy Helper 未健康,先修复 helper 再部署。';
|
||||
if (!version) return '当前来源还没有可部署版本。';
|
||||
return '版本可用,点击按钮即可通过 helper 发起滚动更新。';
|
||||
};
|
||||
const githubDeployState = describeGitHubDeployState({
|
||||
enabled: config.enabled && config.githubReleasesEnabled,
|
||||
helperHealthy,
|
||||
helperError: status?.helper?.error,
|
||||
currentVersion: status?.currentVersion,
|
||||
helperImageTag: status?.helper?.imageTag,
|
||||
candidate: status?.githubRelease,
|
||||
});
|
||||
const dockerDeployState = describeDockerDeployState({
|
||||
enabled: config.enabled && config.dockerHubTagsEnabled,
|
||||
helperHealthy,
|
||||
helperError: status?.helper?.error,
|
||||
currentVersion: status?.currentVersion,
|
||||
helper: status?.helper,
|
||||
candidate: status?.dockerHubTag,
|
||||
});
|
||||
const updateReminder = buildUpdateReminder({
|
||||
currentVersion: status?.currentVersion,
|
||||
helper: status?.helper,
|
||||
githubRelease: status?.githubRelease,
|
||||
dockerHubTag: status?.dockerHubTag,
|
||||
});
|
||||
const canDeployGithub = !deploying && githubDeployState.canDeploy;
|
||||
const canDeployDocker = !deploying && dockerDeployState.canDeploy;
|
||||
const helperHistory = Array.isArray(status?.helper?.history) ? status.helper.history : [];
|
||||
const historyPreview = helperHistory.slice(0, 2);
|
||||
const currentRevision = String(status?.helper?.revision || '').trim();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -405,10 +417,18 @@ export default function UpdateCenterSection() {
|
||||
return (
|
||||
<div className="card" style={{ padding: 20 }}>
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 6 }}>更新中心</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap', marginBottom: 6 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 14 }}>更新中心</div>
|
||||
<span className={`${updateReminder.badgeClassName} ${updateReminder.highlight ? 'stat-value-glow' : ''}`.trim()}>
|
||||
{updateReminder.label}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.55 }}>
|
||||
在设置页里统一查看 GitHub Releases、Docker Hub 版本和 K3s helper 状态,避免部署信息散落在多个入口。
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-secondary)', lineHeight: 1.5, marginTop: 6 }}>
|
||||
{updateReminder.detail}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -612,17 +632,20 @@ export default function UpdateCenterSection() {
|
||||
<span className={getSourceBadge(config.githubReleasesEnabled, status?.githubRelease?.normalizedVersion).className}>
|
||||
{getSourceBadge(config.githubReleasesEnabled, status?.githubRelease?.normalizedVersion).label}
|
||||
</span>
|
||||
<span className={`${githubDeployState.badgeClassName} ${githubDeployState.highlight ? 'stat-value-glow' : ''}`.trim()}>
|
||||
{githubDeployState.badgeLabel}
|
||||
</span>
|
||||
{config.defaultDeploySource === 'github-release' ? (
|
||||
<span className="badge badge-info">默认来源</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ ...fieldHintStyle, marginBottom: 10 }}>优先使用仓库稳定版 release,适合保留语义化版本节奏。</div>
|
||||
<div style={{ ...summaryValueStyle, fontFamily: 'var(--font-mono)', marginBottom: 8 }}>
|
||||
<div className={githubDeployState.highlight ? 'stat-value-glow' : ''} style={{ ...summaryValueStyle, fontFamily: 'var(--font-mono)', marginBottom: 8 }}>
|
||||
{status?.githubRelease?.displayVersion || status?.githubRelease?.normalizedVersion || '未发现'}
|
||||
</div>
|
||||
<div style={{ ...fieldHintStyle, marginBottom: 12 }}>
|
||||
{renderDeployReason(config.githubReleasesEnabled, status?.githubRelease?.normalizedVersion, status?.helper?.error)}
|
||||
{githubDeployState.reason}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -648,17 +671,20 @@ export default function UpdateCenterSection() {
|
||||
<span className={getSourceBadge(config.dockerHubTagsEnabled, status?.dockerHubTag?.normalizedVersion).className}>
|
||||
{getSourceBadge(config.dockerHubTagsEnabled, status?.dockerHubTag?.normalizedVersion).label}
|
||||
</span>
|
||||
<span className={`${dockerDeployState.badgeClassName} ${dockerDeployState.highlight ? 'stat-value-glow' : ''}`.trim()}>
|
||||
{dockerDeployState.badgeLabel}
|
||||
</span>
|
||||
{config.defaultDeploySource === 'docker-hub-tag' ? (
|
||||
<span className="badge badge-info">默认来源</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ ...fieldHintStyle, marginBottom: 10 }}>适合镜像标签领先于 release 的场景,直接跟随容器分发节奏。</div>
|
||||
<div style={{ ...summaryValueStyle, fontFamily: 'var(--font-mono)', marginBottom: 8 }}>
|
||||
<div className={dockerDeployState.highlight ? 'stat-value-glow' : ''} style={{ ...summaryValueStyle, fontFamily: 'var(--font-mono)', marginBottom: 8 }}>
|
||||
{status?.dockerHubTag?.displayVersion || status?.dockerHubTag?.normalizedVersion || '未发现'}
|
||||
</div>
|
||||
<div style={{ ...fieldHintStyle, marginBottom: 12 }}>
|
||||
{renderDeployReason(config.dockerHubTagsEnabled, status?.dockerHubTag?.normalizedVersion, status?.helper?.error)}
|
||||
{dockerDeployState.reason}
|
||||
</div>
|
||||
<div style={{ ...fieldHintStyle, marginBottom: 12 }}>
|
||||
最近推送:{formatTaskTime(status?.dockerHubTag?.publishedAt)}
|
||||
@@ -730,63 +756,41 @@ export default function UpdateCenterSection() {
|
||||
<div style={{ ...sectionPanelStyle, marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 10, flexWrap: 'wrap' }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 13 }}>回退历史</div>
|
||||
<span className="badge badge-muted">最近 revision</span>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
<span className="badge badge-muted">最近 revision</span>
|
||||
{helperHistory.length > historyPreview.length ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost"
|
||||
style={{ border: '1px solid var(--color-border)', padding: '4px 10px', minHeight: 0 }}
|
||||
onClick={() => setHistoryModalOpen(true)}
|
||||
>
|
||||
展开全部 {helperHistory.length} 条
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 10 }}>
|
||||
这里展示 helper 读到的最近 Helm revision。支持直接回退到某个旧 revision,适合快速滚回上一次稳定镜像。
|
||||
页面里默认只保留最近 revision 预览,完整历史放进弹窗,避免设置页被长回退列表拖得过长。
|
||||
</div>
|
||||
{status?.helper?.history && status.helper.history.length > 0 ? (
|
||||
{helperHistory.length > 0 ? (
|
||||
<div style={{ display: 'grid', gap: 10 }}>
|
||||
{status.helper.history.map((entry) => {
|
||||
{historyPreview.map((entry) => {
|
||||
const revision = String(entry?.revision || '').trim();
|
||||
const isCurrentRevision = revision && revision === String(status?.helper?.revision || '').trim();
|
||||
return (
|
||||
<div
|
||||
<UpdateCenterHistoryEntryCard
|
||||
key={revision || 'unknown-revision'}
|
||||
style={{
|
||||
border: '1px solid var(--color-border-light)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
padding: 12,
|
||||
display: 'grid',
|
||||
gap: 6,
|
||||
background: 'var(--color-bg-card)',
|
||||
entry={entry}
|
||||
currentRevision={currentRevision}
|
||||
helperHealthy={helperHealthy}
|
||||
deploying={deploying}
|
||||
compact
|
||||
formatTaskTime={formatTaskTime}
|
||||
formatImageTarget={formatImageTarget}
|
||||
onRollback={(nextRevision) => {
|
||||
void runRollback(nextRevision);
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text-primary)' }}>
|
||||
revision {revision || '-'}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
{entry?.status ? (
|
||||
<span className="badge badge-muted">{entry.status}</span>
|
||||
) : null}
|
||||
{isCurrentRevision ? (
|
||||
<span className="badge badge-info">当前运行</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ ...summaryValueStyle, fontFamily: 'var(--font-mono)', fontSize: 13 }}>
|
||||
{formatImageTarget(entry?.imageTag, entry?.imageDigest) || '未记录镜像信息'}
|
||||
</div>
|
||||
{entry?.description ? (
|
||||
<div style={fieldHintStyle}>{entry.description}</div>
|
||||
) : null}
|
||||
<div style={fieldHintStyle}>更新时间:{formatTaskTime(entry?.updatedAt)}</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isCurrentRevision) return;
|
||||
void runRollback(revision);
|
||||
}}
|
||||
disabled={!helperHealthy || deploying || isCurrentRevision || !revision}
|
||||
className="btn btn-ghost"
|
||||
style={{ border: '1px solid var(--color-border)' }}
|
||||
>
|
||||
回退到 revision {revision}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -845,6 +849,20 @@ export default function UpdateCenterSection() {
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginTop: 10 }}>
|
||||
最近状态:{visibleTaskStatus}
|
||||
</div>
|
||||
<UpdateCenterHistoryModal
|
||||
open={historyModalOpen}
|
||||
helperHealthy={helperHealthy}
|
||||
deploying={deploying}
|
||||
currentRevision={currentRevision}
|
||||
history={helperHistory}
|
||||
formatTaskTime={formatTaskTime}
|
||||
formatImageTarget={formatImageTarget}
|
||||
onClose={() => setHistoryModalOpen(false)}
|
||||
onRollback={(revision) => {
|
||||
setHistoryModalOpen(false);
|
||||
void runRollback(revision);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user