fix: surface hash-duplicate warning and add manual refresh
Three issues fixed:
1. **Skipped-commit warning not visible** — When the user commits
unchanged content (hash matches most recent version), the backend
correctly returns {skipped: true, reason: "unchanged", message: ...}
but the frontend Notification.warning was easy to miss (default
4-5s autoClose, small toast at top-right). Now uses {autoClose: false}
so the warning stays until manually dismissed, plus a console.log
of the raw commit response for debugging. Added a fallback branch
that warns clearly if the response shape is unexpected.
2. **No manual refresh on the panel** — In S3 setups the list call
can occasionally lag the just-committed entry (manifest strong
consistency is a per-implementation property). Added a "刷新"
button at the top of the sidebar that calls model.refresh(), letting
the user force a re-fetch when the auto-refresh misses.
3. **S3 addressing_style** — user-side config addition: S3Storage
now uses path-style addressing (endpoint/bucket/key) which is the
format required by MinIO and several other S3-compatible services.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -82,7 +82,10 @@ class S3Storage(StorageAdapter):
|
|||||||
aws_access_key_id=access_key,
|
aws_access_key_id=access_key,
|
||||||
aws_secret_access_key=secret_key,
|
aws_secret_access_key=secret_key,
|
||||||
region_name="us-east-1",
|
region_name="us-east-1",
|
||||||
config=BotoConfig(signature_version="s3v4"),
|
config=BotoConfig(
|
||||||
|
signature_version='s3v4',
|
||||||
|
s3={'addressing_style': 'path'},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self._ClientError = ClientError
|
self._ClientError = ClientError
|
||||||
self._connect_and_ensure_bucket()
|
self._connect_and_ensure_bucket()
|
||||||
|
|||||||
+19
-4
@@ -64,12 +64,27 @@ export function registerCommands(
|
|||||||
name,
|
name,
|
||||||
description
|
description
|
||||||
);
|
);
|
||||||
if (result.skipped) {
|
// eslint-disable-next-line no-console
|
||||||
// Content hash matches the most recent version — nothing to save.
|
console.log('[snapshot] commit response:', result);
|
||||||
Notification.warning(result.message ?? '内容未变更,已跳过保存');
|
if (result && result.skipped) {
|
||||||
} else if (result.entry) {
|
// Hash matched the most recent version — nothing to save. Use
|
||||||
|
// autoClose:false so the warning stays visible until the user
|
||||||
|
// dismisses it (the default 4-5s timeout is easy to miss).
|
||||||
|
Notification.warning(
|
||||||
|
result.message ?? '内容未变更,已跳过保存',
|
||||||
|
{ autoClose: false }
|
||||||
|
);
|
||||||
|
} else if (result && result.entry) {
|
||||||
await model.refresh();
|
await model.refresh();
|
||||||
Notification.success(`已保存快照:${name}`);
|
Notification.success(`已保存快照:${name}`);
|
||||||
|
} else {
|
||||||
|
// Unexpected response shape — surface clearly so the user is not
|
||||||
|
// left wondering whether the save happened.
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('[snapshot] unexpected commit response shape:', result);
|
||||||
|
Notification.warning('保存响应格式异常,请查看浏览器控制台', {
|
||||||
|
autoClose: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Notification.error(`保存快照失败:${(e as Error).message}`);
|
Notification.error(`保存快照失败:${(e as Error).message}`);
|
||||||
|
|||||||
@@ -46,70 +46,84 @@ const Panel: React.FC<PanelProps> = ({ model, app, tracker, serverSettings, onOp
|
|||||||
if (!model.path) {
|
if (!model.path) {
|
||||||
return <div className="jp-snapshot-panel-empty">未选中 notebook</div>;
|
return <div className="jp-snapshot-panel-empty">未选中 notebook</div>;
|
||||||
}
|
}
|
||||||
if (model.loading && model.versions.length === 0) {
|
|
||||||
return <div className="jp-snapshot-panel-empty">加载中…</div>;
|
|
||||||
}
|
|
||||||
if (model.versions.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="jp-snapshot-panel-empty">
|
|
||||||
<div>暂无快照</div>
|
|
||||||
<div className="jp-snapshot-panel-hint">点击工具栏「保存快照」按钮创建</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<ul className="jp-snapshot-panel-list">
|
<div className="jp-snapshot-panel-container">
|
||||||
{model.versions.map(v => (
|
<div className="jp-snapshot-panel-toolbar">
|
||||||
<li key={v.id} className="jp-snapshot-panel-item">
|
<button
|
||||||
<div className="jp-snapshot-panel-item-header">
|
className="jp-snapshot-panel-refresh-button"
|
||||||
<span className="jp-snapshot-panel-name" title={v.name}>
|
onClick={() => {
|
||||||
{v.name}
|
void model.refresh();
|
||||||
</span>
|
}}
|
||||||
<span className="jp-snapshot-panel-size">{formatSize(v.size)}</span>
|
disabled={model.loading}
|
||||||
|
title="重新拉取版本列表"
|
||||||
|
>
|
||||||
|
{model.loading ? '刷新中…' : '刷新'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{model.loading && model.versions.length === 0 ? (
|
||||||
|
<div className="jp-snapshot-panel-empty">加载中…</div>
|
||||||
|
) : model.versions.length === 0 ? (
|
||||||
|
<div className="jp-snapshot-panel-empty">
|
||||||
|
<div>暂无快照</div>
|
||||||
|
<div className="jp-snapshot-panel-hint">
|
||||||
|
点击工具栏「保存快照」按钮创建
|
||||||
</div>
|
</div>
|
||||||
{v.description ? (
|
</div>
|
||||||
<div className="jp-snapshot-panel-desc">{v.description}</div>
|
) : (
|
||||||
) : null}
|
<ul className="jp-snapshot-panel-list">
|
||||||
<div className="jp-snapshot-panel-time">{formatTimestamp(v.timestamp)}</div>
|
{model.versions.map(v => (
|
||||||
<div className="jp-snapshot-panel-actions">
|
<li key={v.id} className="jp-snapshot-panel-item">
|
||||||
<button
|
<div className="jp-snapshot-panel-item-header">
|
||||||
className="jp-snapshot-panel-button"
|
<span className="jp-snapshot-panel-name" title={v.name}>
|
||||||
onClick={async () => {
|
{v.name}
|
||||||
const panel = tracker.currentWidget;
|
</span>
|
||||||
if (!panel) {
|
<span className="jp-snapshot-panel-size">{formatSize(v.size)}</span>
|
||||||
return;
|
</div>
|
||||||
}
|
{v.description ? (
|
||||||
const notebookModel = panel.content.model;
|
<div className="jp-snapshot-panel-desc">{v.description}</div>
|
||||||
if (!notebookModel) {
|
) : null}
|
||||||
return;
|
<div className="jp-snapshot-panel-time">{formatTimestamp(v.timestamp)}</div>
|
||||||
}
|
<div className="jp-snapshot-panel-actions">
|
||||||
try {
|
<button
|
||||||
const oldNb = (await getSnapshotContent(
|
className="jp-snapshot-panel-button"
|
||||||
serverSettings,
|
onClick={async () => {
|
||||||
model.path,
|
const panel = tracker.currentWidget;
|
||||||
v.id
|
if (!panel) {
|
||||||
)) as INotebook;
|
return;
|
||||||
const newNb = notebookModel.toJSON() as unknown as INotebook;
|
}
|
||||||
onOpenDiff(newNb, oldNb, v.name);
|
const notebookModel = panel.content.model;
|
||||||
} catch (e) {
|
if (!notebookModel) {
|
||||||
console.error('Diff failed', e);
|
return;
|
||||||
}
|
}
|
||||||
}}
|
try {
|
||||||
>
|
const oldNb = (await getSnapshotContent(
|
||||||
对比
|
serverSettings,
|
||||||
</button>
|
model.path,
|
||||||
<button
|
v.id
|
||||||
className="jp-snapshot-panel-button"
|
)) as INotebook;
|
||||||
onClick={() => {
|
const newNb = notebookModel.toJSON() as unknown as INotebook;
|
||||||
void app.commands.execute(CommandIDs.restore, { id: v.id });
|
onOpenDiff(newNb, oldNb, v.name);
|
||||||
}}
|
} catch (e) {
|
||||||
>
|
console.error('Diff failed', e);
|
||||||
恢复
|
}
|
||||||
</button>
|
}}
|
||||||
</div>
|
>
|
||||||
</li>
|
对比
|
||||||
))}
|
</button>
|
||||||
</ul>
|
<button
|
||||||
|
className="jp-snapshot-panel-button"
|
||||||
|
onClick={() => {
|
||||||
|
void app.commands.execute(CommandIDs.restore, { id: v.id });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
恢复
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,37 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jp-snapshot-panel-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jp-snapshot-panel-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jp-snapshot-panel-refresh-button {
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 1px solid var(--jp-border-color2, #ccc);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--jp-layout-color2, #f5f5f5);
|
||||||
|
color: var(--jp-ui-font-color1, #333);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jp-snapshot-panel-refresh-button:hover:not(:disabled) {
|
||||||
|
background: var(--jp-layout-color3, #e8e8e8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jp-snapshot-panel-refresh-button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.jp-snapshot-panel-empty {
|
.jp-snapshot-panel-empty {
|
||||||
color: var(--jp-ui-font-color2, #555);
|
color: var(--jp-ui-font-color2, #555);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user