fix(connection): clear warnings after metadata recovery
This commit is contained in:
parent
9ba61756db
commit
d41a6d8944
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { AlertTriangle, X } from "@lucide/vue";
|
||||
import { AlertTriangle } from "@lucide/vue";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
|
||||
|
|
@ -29,7 +29,14 @@ function clearError() {
|
|||
<template>
|
||||
<Popover v-if="errorMessage">
|
||||
<PopoverTrigger as-child>
|
||||
<button type="button" class="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded text-amber-500 hover:bg-amber-500/10 hover:text-amber-600 focus:outline-none focus:ring-1 focus:ring-amber-500/40" :class="triggerClass" :title="t('connection.lastError')" @click.stop>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center rounded text-amber-500 hover:bg-amber-500/10 hover:text-amber-600 focus:outline-none focus:ring-1 focus:ring-amber-500/40"
|
||||
:class="triggerClass"
|
||||
:title="t('connection.errorIndicatorHint')"
|
||||
:aria-label="t('connection.errorIndicatorHint')"
|
||||
@click.stop
|
||||
>
|
||||
<AlertTriangle class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
|
|
@ -43,9 +50,7 @@ function clearError() {
|
|||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground" :title="t('connection.clearError')" @click="clearError">
|
||||
<X class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button type="button" class="shrink-0 rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:bg-muted hover:text-foreground" @click="clearError">{{ t("connection.clearError") }}</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
|
|
|||
|
|
@ -420,6 +420,7 @@ export default {
|
|||
agentDriverUpdateConnectionHint: "A built-in driver update is available for this connection. The connection failure may be related to an outdated local driver. Update the corresponding driver in Driver Manager, then retry.",
|
||||
jdbcPluginNotInstalled: "JDBC plugin is not installed. Install the optional JDBC plugin to use this connection.",
|
||||
lastError: "Connection error",
|
||||
errorIndicatorHint: "Connection error. Click to view details",
|
||||
clearError: "Clear connection error",
|
||||
saveFailed: "Failed to save connection: {message}",
|
||||
deleted: "Connection deleted",
|
||||
|
|
|
|||
|
|
@ -335,6 +335,7 @@ export default withEnglishFallback({
|
|||
"Hay una actualización de controlador integrada disponible para esta conexión. El error de conexión puede estar relacionado con un controlador local desactualizado. Actualiza el controlador correspondiente en el Administrador de controladores y vuelve a intentarlo.",
|
||||
jdbcPluginNotInstalled: "El plugin JDBC no está instalado. Instale el plugin JDBC opcional para usar esta conexión.",
|
||||
lastError: "Error de conexión",
|
||||
errorIndicatorHint: "Error de conexión. Haz clic para ver los detalles",
|
||||
clearError: "Limpiar error de conexión",
|
||||
saveFailed: "Error al guardar la conexión: {message}",
|
||||
deleted: "Conexión eliminada",
|
||||
|
|
|
|||
|
|
@ -333,6 +333,7 @@ export default withEnglishFallback({
|
|||
agentDriverUpdateConnectionHint: "È disponibile un aggiornamento del driver integrato per questa connessione. Il problema di connessione potrebbe essere correlato a un driver locale obsoleto. Aggiorna il driver corrispondente in Gestione Driver e riprova.",
|
||||
jdbcPluginNotInstalled: "Il plugin JDBC non è installato. Installa il plugin JDBC opzionale per utilizzare questa connessione.",
|
||||
lastError: "Errore di connessione",
|
||||
errorIndicatorHint: "Errore di connessione. Fai clic per visualizzare i dettagli",
|
||||
clearError: "Cancella errore di connessione",
|
||||
saveFailed: "Impossibile salvare la connessione: {message}",
|
||||
deleted: "Connessione eliminata",
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@ export default withEnglishFallback({
|
|||
agentDriverUpdateConnectionHint: "この接続で使用中の内蔵ドライバに更新が利用可能です。接続失敗はローカルドライバの旧版が原因の可能性があります。ドライバマネージャーで該当ドライバを更新してから再試行してください。",
|
||||
jdbcPluginNotInstalled: "JDBCプラグインがインストールされていません。この接続を使用するにはオプションのJDBCプラグインをインストールしてください。",
|
||||
lastError: "接続エラー",
|
||||
errorIndicatorHint: "接続エラー。クリックして詳細を表示",
|
||||
clearError: "接続エラーをクリア",
|
||||
saveFailed: "接続の保存に失敗しました: {message}",
|
||||
deleted: "接続を削除しました",
|
||||
|
|
|
|||
|
|
@ -334,6 +334,7 @@ export default withEnglishFallback({
|
|||
agentDriverUpdateConnectionHint: "Há uma atualização de driver integrada disponível para esta conexão. A falha de conexão pode estar relacionada a um driver local desatualizado. Atualize o driver correspondente no Gerenciador de Drivers e tente novamente.",
|
||||
jdbcPluginNotInstalled: "O plugin JDBC não está instalado. Instale o plugin JDBC opcional para usar esta conexão.",
|
||||
lastError: "Erro de conexão",
|
||||
errorIndicatorHint: "Erro de conexão. Clique para ver os detalhes",
|
||||
clearError: "Limpar erro de conexão",
|
||||
saveFailed: "Falha ao salvar a conexão: {message}",
|
||||
deleted: "Conexão excluída",
|
||||
|
|
|
|||
|
|
@ -424,6 +424,7 @@ export default withEnglishFallback({
|
|||
agentDriverUpdateConnectionHint: "当前连接使用的内置驱动有可用更新,连接失败可能与本地驱动版本过旧有关。请在「驱动管理」中更新对应驱动后重试。",
|
||||
jdbcPluginNotInstalled: "JDBC 插件未安装,请先安装 JDBC 插件再使用此连接。",
|
||||
lastError: "连接错误",
|
||||
errorIndicatorHint: "连接错误,点击查看详情",
|
||||
clearError: "清除连接错误",
|
||||
saveFailed: "保存连接失败:{message}",
|
||||
deleted: "连接已删除",
|
||||
|
|
|
|||
|
|
@ -334,6 +334,7 @@ export default withEnglishFallback({
|
|||
agentDriverUpdateConnectionHint: "目前連線使用的內建驅動有可用更新,連線失敗可能與本機驅動版本過舊有關。請在「驅動管理」中更新對應驅動後重試。",
|
||||
jdbcPluginNotInstalled: "JDBC 外掛程式未安裝,請先安裝 JDBC 外掛程式再使用此連線。",
|
||||
lastError: "連線錯誤",
|
||||
errorIndicatorHint: "連線錯誤,點擊檢視詳情",
|
||||
clearError: "清除連線錯誤",
|
||||
saveFailed: "儲存連線失敗:{message}",
|
||||
deleted: "連線已刪除",
|
||||
|
|
|
|||
|
|
@ -212,6 +212,187 @@ describe("connectionStore metadata loading", () => {
|
|||
expect(schemaNode.children?.map((node) => node.label)).toEqual(["users"]);
|
||||
});
|
||||
|
||||
it("clears a stale connection error after a schema metadata retry succeeds", async () => {
|
||||
const listSchemaInfos = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("connection slots exhausted"))
|
||||
.mockResolvedValueOnce([{ name: "public", comment: null }]);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listInstalledAgents: vi.fn().mockResolvedValue([]),
|
||||
listSchemaInfos,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = postgresConnection();
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: `${connection.id}:app`,
|
||||
label: "app",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
await expect(store.loadSchemas(connection.id, "app", { force: true })).rejects.toThrow("connection slots exhausted");
|
||||
expect(store.connectionErrors[connection.id]).toBe("connection slots exhausted");
|
||||
|
||||
await store.loadSchemas(connection.id, "app", { force: true });
|
||||
|
||||
expect(store.connectionErrors[connection.id]).toBeUndefined();
|
||||
expect(store.treeNodes[0]?.children?.[0]?.children?.map((node) => node.label)).toEqual(["public"]);
|
||||
});
|
||||
|
||||
it("clears a failed metadata warning when the driver hint finishes during retry", async () => {
|
||||
let resolveAgents!: (drivers: Array<{ db_type: string; installed: boolean; update_available: boolean }>) => void;
|
||||
let resolveSchemas!: (schemas: Array<{ name: string; comment: null }>) => void;
|
||||
const listInstalledAgents = vi.fn(
|
||||
() =>
|
||||
new Promise<Array<{ db_type: string; installed: boolean; update_available: boolean }>>((resolve) => {
|
||||
resolveAgents = resolve;
|
||||
}),
|
||||
);
|
||||
const listSchemaInfos = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("connection slots exhausted"))
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<Array<{ name: string; comment: null }>>((resolve) => {
|
||||
resolveSchemas = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listInstalledAgents,
|
||||
listSchemaInfos,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = { ...postgresConnection(), db_type: "oracle" } as ConnectionConfig;
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: `${connection.id}:app`,
|
||||
label: "app",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
await expect(store.loadSchemas(connection.id, "app", { force: true })).rejects.toThrow("connection slots exhausted");
|
||||
expect(store.connectionErrors[connection.id]).toBe("connection slots exhausted");
|
||||
|
||||
const retry = store.loadSchemas(connection.id, "app", { force: true });
|
||||
await vi.waitFor(() => expect(listSchemaInfos).toHaveBeenCalledTimes(2));
|
||||
|
||||
resolveAgents([{ db_type: "oracle", installed: true, update_available: true }]);
|
||||
await vi.waitFor(() => expect(store.connectionErrors[connection.id]).toContain("built-in driver update"));
|
||||
|
||||
resolveSchemas([{ name: "public", comment: null }]);
|
||||
await retry;
|
||||
|
||||
expect(store.connectionErrors[connection.id]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not clear a newer error when an older metadata request succeeds", async () => {
|
||||
let resolveSchemas!: (schemas: Array<{ name: string; comment: null }>) => void;
|
||||
const listSchemaInfos = vi.fn(
|
||||
() =>
|
||||
new Promise<Array<{ name: string; comment: null }>>((resolve) => {
|
||||
resolveSchemas = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listSchemaInfos,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = postgresConnection();
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: `${connection.id}:app`,
|
||||
label: "app",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
store.setConnectionError(connection.id, "old error");
|
||||
|
||||
const load = store.loadSchemas(connection.id, "app", { force: true });
|
||||
await vi.waitFor(() => expect(listSchemaInfos).toHaveBeenCalledOnce());
|
||||
store.setConnectionError(connection.id, "newer error");
|
||||
resolveSchemas([{ name: "public", comment: null }]);
|
||||
await load;
|
||||
|
||||
expect(store.connectionErrors[connection.id]).toBe("newer error");
|
||||
});
|
||||
|
||||
it("keeps an expanding schema attached while its parent refreshes", async () => {
|
||||
const listSchemaInfos = vi.fn().mockResolvedValue([{ name: "core", comment: null }]);
|
||||
|
||||
|
|
|
|||
|
|
@ -352,6 +352,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
const cancelledLocalConnectionAttempts = new Map<string, Set<number>>();
|
||||
const successfulLocalConnectionAttempts = new Map<string, number>();
|
||||
const connectionStateRevisions = new Map<string, number>();
|
||||
const connectionErrorRevisions = new Map<string, number>();
|
||||
let nextLocalConnectionAttempt = 0;
|
||||
let beforeConnectHandler: BeforeConnectHandler | null = null;
|
||||
let initFromDiskPromise: Promise<void> | null = null;
|
||||
|
|
@ -375,7 +376,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
}
|
||||
|
||||
logMetadataLoadTrace(metadataTraceLogger, trace, "cache-miss", { cacheStatus: options?.force ? "refresh" : "miss", force: options?.force === true });
|
||||
const errorRevision = connectionErrorRevision(scope.connectionId);
|
||||
const result = await load();
|
||||
clearConnectionErrorIfUnchanged(scope.connectionId, errorRevision);
|
||||
metadataListPageCache.set(scope, result);
|
||||
logMetadataLoadTrace(metadataTraceLogger, trace, "done", {
|
||||
cacheStatus: options?.force ? "refresh" : "miss",
|
||||
|
|
@ -610,6 +613,22 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
|
||||
function setConnectionError(connectionId: string, message: string) {
|
||||
connectionErrors.value[connectionId] = message;
|
||||
connectionErrorRevisions.set(connectionId, connectionErrorRevision(connectionId) + 1);
|
||||
}
|
||||
|
||||
function updateConnectionErrorPresentation(connectionId: string, expectedMessage: string, message: string) {
|
||||
// Hints decorate the current error event and must not invalidate a retry's recovery revision.
|
||||
if (connectionErrors.value[connectionId] !== expectedMessage) return;
|
||||
connectionErrors.value[connectionId] = message;
|
||||
}
|
||||
|
||||
function connectionErrorRevision(connectionId?: string | null): number {
|
||||
return connectionId ? (connectionErrorRevisions.get(connectionId) ?? 0) : 0;
|
||||
}
|
||||
|
||||
function clearConnectionErrorIfUnchanged(connectionId: string | null | undefined, revision: number) {
|
||||
if (!connectionId || connectionErrorRevision(connectionId) !== revision) return;
|
||||
clearConnectionError(connectionId);
|
||||
}
|
||||
|
||||
function agentDriverUpdateHint(): string {
|
||||
|
|
@ -641,19 +660,20 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
const config = getConfig(connectionId);
|
||||
const message = connectionErrorWithDriverUpdateHint(config, baseMessage);
|
||||
if (message !== baseMessage) {
|
||||
setConnectionError(connectionId, message);
|
||||
updateConnectionErrorPresentation(connectionId, baseMessage, message);
|
||||
return;
|
||||
}
|
||||
void refreshAgentDriversForErrorHint().then(() => {
|
||||
if (connectionErrors.value[connectionId] !== baseMessage) return;
|
||||
const refreshedMessage = connectionErrorWithDriverUpdateHint(config, baseMessage);
|
||||
if (refreshedMessage !== baseMessage) setConnectionError(connectionId, refreshedMessage);
|
||||
if (refreshedMessage !== baseMessage) updateConnectionErrorPresentation(connectionId, baseMessage, refreshedMessage);
|
||||
});
|
||||
}
|
||||
|
||||
function clearConnectionError(connectionId: string) {
|
||||
if (!connectionErrors.value[connectionId]) return;
|
||||
delete connectionErrors.value[connectionId];
|
||||
connectionErrorRevisions.set(connectionId, connectionErrorRevision(connectionId) + 1);
|
||||
}
|
||||
|
||||
function markConnectionHealthChecked(connectionId: string) {
|
||||
|
|
@ -703,9 +723,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
|
||||
async function withMetadataLoadTimeout<T>(connectionId: string, promise: Promise<T>, label: string): Promise<T> {
|
||||
const timeoutMs = metadataLoadTimeoutMs(getConfig(connectionId));
|
||||
const errorRevision = connectionErrorRevision(connectionId);
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
return await Promise.race([
|
||||
const result = await Promise.race([
|
||||
promise,
|
||||
new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
|
|
@ -713,6 +734,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
}, timeoutMs);
|
||||
}),
|
||||
]);
|
||||
clearConnectionErrorIfUnchanged(connectionId, errorRevision);
|
||||
return result;
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
|
|
@ -1753,6 +1776,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
persistPinnedTreeNodeIds();
|
||||
for (const id of removedIds) {
|
||||
clearConnectionError(id);
|
||||
connectionErrorRevisions.delete(id);
|
||||
connectedIds.value.delete(id);
|
||||
clearConnectionIdentifierQuote(id);
|
||||
clearConnectionHealthCheck(id);
|
||||
|
|
@ -2821,7 +2845,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
}
|
||||
}
|
||||
|
||||
const schemas = sortSidebarSchemaInfos(await api.listSchemaInfos(connectionId, database));
|
||||
const schemas = sortSidebarSchemaInfos(await withMetadataLoadTimeout(connectionId, api.listSchemaInfos(connectionId, database), "schemas"));
|
||||
const visibleSchemaNames = new Set(
|
||||
filterSchemaNamesForConnection(
|
||||
schemas.map((schema) => schema.name),
|
||||
|
|
|
|||
Loading…
Reference in New Issue