fix(frontend): harden auth request layer per web guide
- fix double-unwrap bug in useLogin prefs sync (theme sync silently broken) - request layer: 401 single-flight refresh queue + debounced single redirect with return-url, cancel-aware silent passthrough, skipErrorHandler opt-out, typed blob download (replace bare fetch export in QueriesPage) - useFetchList: AbortController + version guard + error state (three states) - ErrorBoundary with chunk-load auto recovery (reload once) - permission single source: derive route guard from menu constants, drop hand-maintained routePermissionMap; TicketDetailPage uses unified hasActionPermission instead of inline role string comparison - namespaced localStorage keys (opsxsql.*) with read-old-write-new migration - fix pre-existing tsc errors (InstancePage/WorkflowPage/QueriesPage/ PlatformConfigPage/RedisDetailPage) - typecheck now passes clean
This commit is contained in:
1 parent
0fcdbc5f6c
commit
d5b8b8116c
19 files changed
+502
-110
No files matched your search
@@ -26,6 +26,8 @@ import { QueryHistoryDrawer } from '@/features/query/components/QueryHistoryDraw
|
||||
import { QueryTemplatePanel } from '@/features/query/components/QueryTemplatePanel';
|
||||
import type { QueryHistoryItem } from '@/features/query/types';
|
||||
import { downloadCSV, exportToCSV } from '@/features/query/utils';
|
||||
import api from '@/api/request';
|
||||
import { readStorage, writeStorage } from '@/lib/storage';
|
||||
|
||||
interface QueryResult {
|
||||
columns: string[];
|
||||
@@ -136,13 +138,13 @@ export default function QueriesPage() {
|
||||
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 50 });
|
||||
const resultContainerRef = useRef<HTMLDivElement>(null);
|
||||
// -- Sidebar 拖拽宽度 --
|
||||
const [sideWidth, setSideWidth] = useState(() => Number(localStorage.getItem('query-side-width')) || 280);
|
||||
const [sideWidth, setSideWidth] = useState(() => Number(readStorage('querySideWidth')) || 280);
|
||||
const sidebarDragRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// -- 历史 state --
|
||||
const [history, setHistory] = useState<QueryHistoryItem[]>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('query_favorites');
|
||||
const saved = readStorage('queryFavorites');
|
||||
const favIds = saved ? new Set<number>(JSON.parse(saved)) : new Set<number>();
|
||||
const initialHistory: QueryHistoryItem[] = [];
|
||||
return initialHistory.map(h => ({ ...h, favorite: favIds.has(h.id) }));
|
||||
@@ -237,7 +239,7 @@ export default function QueriesPage() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!instanceId || !database || (dbType !== 'mysql' && dbType !== 'postgres' && dbType !== 'postgresql')) return;
|
||||
if (!instanceId || !database || (dbType !== 'mysql' && dbType !== 'postgresql')) return;
|
||||
if (/^\d+$/.test(database)) return;
|
||||
instanceDetailApi.getDbSchema(Number(instanceId), database)
|
||||
.then(data => { if (data && typeof data === 'object') setEditorSchema(prev => ({ ...prev, [database]: data as Record<string, string[]> })); })
|
||||
@@ -330,13 +332,11 @@ export default function QueriesPage() {
|
||||
const handleExportResult = useCallback(async () => {
|
||||
if (!sql || !instanceId) return;
|
||||
try {
|
||||
const resp = await fetch('/api/v1/query/export', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}` },
|
||||
body: JSON.stringify({ instance_id: Number(instanceId), database: database || undefined, sql }),
|
||||
});
|
||||
if (!resp.ok) throw new Error('导出失败');
|
||||
const blob = await resp.blob();
|
||||
const blob = await api.post<unknown, Blob>('/query/export', {
|
||||
instance_id: Number(instanceId),
|
||||
database: database || undefined,
|
||||
sql,
|
||||
}, { responseType: 'blob' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
@@ -364,7 +364,7 @@ export default function QueriesPage() {
|
||||
const toggleFavorite = useCallback((id: number) => {
|
||||
setHistory(prev => {
|
||||
const next = prev.map(h => h.id === id ? { ...h, favorite: !h.favorite } : h);
|
||||
try { localStorage.setItem('query_favorites', JSON.stringify(next.filter(h => h.favorite).map(h => h.id))); } catch { /* noop */ }
|
||||
try { writeStorage('queryFavorites', JSON.stringify(next.filter(h => h.favorite).map(h => h.id))); } catch { /* noop */ }
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
@@ -388,7 +388,7 @@ export default function QueriesPage() {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
setSideWidth(w => {
|
||||
localStorage.setItem('query-side-width', String(w));
|
||||
writeStorage('querySideWidth', String(w));
|
||||
return w;
|
||||
});
|
||||
};
|
||||
@@ -634,7 +634,7 @@ export default function QueriesPage() {
|
||||
<div className="dg-card overflow-hidden flex flex-col">
|
||||
<SqlEditor value={sql} onChange={setSql} placeholder={placeholderMap[dbType]}
|
||||
language={editorLang} height="200px" onExecute={handleExecute}
|
||||
schema={(dbType === 'mysql' || dbType === 'postgres' || dbType === 'postgresql') ? editorSchema : undefined} />
|
||||
schema={(dbType === 'mysql' || dbType === 'postgresql') ? editorSchema : undefined} />
|
||||
<div className="flex items-center justify-between px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border-t border-[var(--dg-border)] text-[11px] dg-text-muted">
|
||||
<span className="flex items-center gap-1.5">
|
||||
{dbTypeIcon[dbType] && <img src={dbTypeIcon[dbType]} alt="" className="w-3 h-3 object-contain" />}
|
||||
|
||||
Reference in new issue
Block a user