Harden image admission, resource recovery and session renewal
Add exact-image compatibility policy, experimental admission, paginated build history and shared reauthentication checks. Preserve uncertain Docker creation records, bound frontend resource waits and renew runtime sessions proactively. Lazy-load and split Monaco, add regression and opt-in live Docker fault tests, and record full race and real-browser acceptance results.
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { ProfileEditor } from './ProfileEditor';
|
||||
import { BackupPanel } from './BackupPanel';
|
||||
import { CreateInstance } from './CreateInstance';
|
||||
import { api, instancePath, type Instance } from './api';
|
||||
import { api, selectableImage, instancePath, type Instance } from './api';
|
||||
import { Panel, ResourceState, Empty, Status, bytes, date, useAction, useResource } from './ui';
|
||||
export function Instances() {
|
||||
const instances = useResource(api.instances, '', 10000), usage = useResource(api.usage);
|
||||
@@ -15,11 +15,11 @@ function InstanceDetail({instance, close, refresh}: {instance: Instance; close:
|
||||
const base = instancePath(instance.id);
|
||||
const stopped = ['stopped', 'exited', 'created'].includes(instance.status);
|
||||
const images = useResource(api.images);
|
||||
const available = images.data?.filter(i => !i.status || i.status === 'available') || [];
|
||||
const available = images.data?.filter(selectableImage) || [];
|
||||
const [upgradeImage, setUpgradeImage] = useState('');
|
||||
const [upgradePassword, setUpgradePassword] = useState('');
|
||||
const profile = useResource(() => api.request<{content:string}>(base + '/profile'));
|
||||
const preview = useResource(() => api.request<{bundles:unknown;config:string;preview_error?:string}>(base + '/profile-preview'));
|
||||
const logs = useResource(() => api.request<{logs:string}>(base + '/logs'));
|
||||
return <Panel title={instance.name + ' · 实例管理'} action={<button onClick={close}>关闭</button>}><div className="tabs">{[['profile','Profile 配置'],['preview','配置预览'],['backups','备份与恢复'],['upgrade','版本升级'],['logs','运行日志']].map(([id,label]) => <button className={tab === id ? 'active' : ''} onClick={() => setTab(id)} key={id}>{label}</button>)}</div>{action.feedback}{tab === 'profile' && <div className="callout">Profile 写入前需先停止实例。当前状态:<Status value={instance.status}/>{!stopped && <button disabled={action.busy} onClick={() => action.run(async () => {await api.request(base + '/stop','POST'); await refresh();}, '停止请求已完成,请确认实例状态。', '停止将中断当前连接,是否继续?')}>停止实例</button>}</div>}{tab === 'profile' && <><p className="muted">profiles/web/cordis.patch.yml · 停止实例后保存,保存后自动启动验证。密钥原文不在此展示。</p><ResourceState resource={profile} />{profile.data && <><label>YAML 配置<ProfileEditor value={content ?? profile.data.content} onChange={setContent} readOnly={action.busy}/></label><div className="actions"><button className="primary" disabled={action.busy || !stopped || content === undefined} onClick={() => action.run(async () => {await api.request(base + '/profile','PUT',{content,restart:true}); await profile.refresh(); setContent(undefined); await refresh();}, '配置已保存并启动验证。', '确认覆盖当前 Profile 配置?')}>保存并启动</button><button disabled={action.busy} onClick={() => action.run(async () => {await api.request(base + '/restart','POST'); await refresh();}, '实例已重启', '重启将中断当前连接,是否继续?')}>重启实例</button></div></>}</>}{tab === 'preview' && <><button onClick={preview.refresh}>刷新预览</button><ResourceState resource={preview}/>{preview.data && <><h3>Bundle 清单</h3><pre>{JSON.stringify(preview.data.bundles,null,2)}</pre>{preview.data.preview_error && <p role="status">{preview.data.preview_error}</p>}<ProfileEditor value={preview.data.config} readOnly label="配置预览(字符串已脱敏)"/></>}</>}{tab === 'backups' && <BackupPanel instance={instance} refresh={async () => {await profile.refresh(); setContent(undefined); await refresh();}}/>}{tab === 'upgrade' && <form className="inline-form" onSubmit={e => {e.preventDefault(); if (!available.some(i => (i.image_id || i.id) === upgradeImage)) return; void action.run(async () => {try {await api.request(base + '/upgrade','POST',{image_id:upgradeImage,password:upgradePassword}); await refresh();} finally {setUpgradePassword('');}}, '实例升级请求已完成。', '确认使用所选本地镜像升级?建议先备份配置。');}}><p>当前版本:{instance.version}</p><label>当前登录密码(加密升级前备份)<input type="password" required autoComplete="current-password" value={upgradePassword} onChange={e => setUpgradePassword(e.target.value)}/></label><ResourceState resource={images}/><label>本地镜像<select required value={upgradeImage} onChange={e => setUpgradeImage(e.target.value)}><option value="" disabled>选择可用镜像</option>{available.map(i => <option key={i.image_id || i.id} value={i.image_id || i.id}>{i.version} · {i.tag || i.image_id || i.id}</option>)}</select></label><button className="primary" disabled={action.busy || !upgradeImage}>升级实例</button></form>}{tab === 'logs' && <><button onClick={logs.refresh} disabled={logs.loading}>刷新日志</button><ResourceState resource={logs}/><pre className="logs">{logs.data?.logs || '暂无日志'}</pre></>}</Panel>;
|
||||
return <Panel title={instance.name + ' · 实例管理'} action={<button onClick={close}>关闭</button>}><div className="tabs">{[['profile','Profile 配置'],['preview','配置预览'],['backups','备份与恢复'],['upgrade','版本升级'],['logs','运行日志']].map(([id,label]) => <button className={tab === id ? 'active' : ''} onClick={() => setTab(id)} key={id}>{label}</button>)}</div>{action.feedback}{tab === 'profile' && <div className="callout">Profile 写入前需先停止实例。当前状态:<Status value={instance.status}/>{!stopped && <button disabled={action.busy} onClick={() => action.run(async () => {await api.request(base + '/stop','POST'); await refresh();}, '停止请求已完成,请确认实例状态。', '停止将中断当前连接,是否继续?')}>停止实例</button>}</div>}{tab === 'profile' && <><p className="muted">profiles/web/cordis.patch.yml · 停止实例后保存,保存后自动启动验证。密钥原文不在此展示。</p><ResourceState resource={profile} />{profile.data && <><label>YAML 配置<ProfileEditor value={content ?? profile.data.content} onChange={setContent} readOnly={action.busy}/></label><div className="actions"><button className="primary" disabled={action.busy || !stopped || content === undefined} onClick={() => action.run(async () => {await api.request(base + '/profile','PUT',{content,restart:true}); await profile.refresh(); setContent(undefined); await refresh();}, '配置已保存并启动验证。', '确认覆盖当前 Profile 配置?')}>保存并启动</button><button disabled={action.busy} onClick={() => action.run(async () => {await api.request(base + '/restart','POST'); await refresh();}, '实例已重启', '重启将中断当前连接,是否继续?')}>重启实例</button></div></>}</>}{tab === 'preview' && <><button onClick={preview.refresh}>刷新预览</button><ResourceState resource={preview}/>{preview.data && <><h3>Bundle 清单</h3><pre>{JSON.stringify(preview.data.bundles,null,2)}</pre>{preview.data.preview_error && <p role="status">{preview.data.preview_error}</p>}<ProfileEditor value={preview.data.config} readOnly label="配置预览(字符串已脱敏)"/></>}</>}{tab === 'backups' && <BackupPanel instance={instance} refresh={async () => {await profile.refresh(); setContent(undefined); await refresh();}}/>}{tab === 'upgrade' && <form className="inline-form" onSubmit={e => {e.preventDefault(); if (!available.some(i => (i.image_id || i.id) === upgradeImage)) return; void action.run(async () => {try {await api.request(base + '/upgrade','POST',{image_id:upgradeImage,password:upgradePassword}); await refresh();} finally {setUpgradePassword('');}}, '实例升级请求已完成。', '确认使用所选本地镜像升级?建议先备份配置。');}}><p>当前版本:{instance.version}</p><p className="callout">仅已准入且本地可用的镜像可用于升级。历史镜像仅允许存量实例运行,不可用于新建或升级。</p><label>当前登录密码(加密升级前备份)<input type="password" required autoComplete="current-password" value={upgradePassword} onChange={e => setUpgradePassword(e.target.value)}/></label><ResourceState resource={images}/><label>本地镜像<select required value={upgradeImage} onChange={e => setUpgradeImage(e.target.value)}><option value="" disabled>选择可用镜像</option>{available.map(i => <option key={i.image_id || i.id} value={i.image_id || i.id}>{i.version} · {i.tag || i.image_id || i.id}</option>)}</select></label><button className="primary" disabled={action.busy || !upgradeImage}>升级实例</button></form>}{tab === 'logs' && <><button onClick={logs.refresh} disabled={logs.loading}>刷新日志</button><ResourceState resource={logs}/><pre className="logs">{logs.data?.logs || '暂无日志'}</pre></>}</Panel>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user