feat(agent): support cancelling installs and upgrades

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-08-18 15:36:52 +08:00
1 parent 5196e92b24
commit c712be3e02
20 files changed
+2030 -193

No files matched your search

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, computed, watch, nextTick } from "vue";
import { useI18n } from "vue-i18n";
import { Activity, ExternalLink, Cpu, FolderOpen, FolderSync, MemoryStick, Search, Square, Trash2, Download, RotateCcw, Loader2, RefreshCw, Check, Clock3, FileUp } from "@lucide/vue";
import { Activity, ExternalLink, Cpu, FolderOpen, FolderSync, MemoryStick, Search, Square, Trash2, Download, RotateCcw, Loader2, RefreshCw, Check, Clock3, FileUp, X } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -23,6 +23,8 @@ import {
addDriverInstallQueue,
driverInstallProgressChannel,
driverInstallProgressPercent,
isDriverInstallCanceledError,
isDriverInstallCancellationTarget,
isDriverInstallProgressForOperation,
isDriverInstallProgressTarget,
removeDriverInstallQueue,
@@ -188,11 +190,13 @@ const agentDriverSearch = ref("");
const selectedDriverCategory = ref<string>("all");
const installing = ref<string | null>(null);
const upgradingAll = ref(false);
const preparingUpgradeAll = ref(false);
const upgradingCompletedCount = ref(0);
const upgradingTotal = ref(0);
const queuedDriverInstalls = ref<string[]>([]);
const reinstallingJre = ref<string | null>(null);
const activeAgentOperationId = ref<string | null>(null);
const cancellableAgentInstallDbType = ref<string | null>(null);
const refreshing = ref(false);
const agentProgressByDbType = reactive<Record<string, DriverInstallProgress | null | undefined>>({});
const jdbcPluginProgress = ref<DriverInstallProgress | null>(null);
@@ -329,6 +333,15 @@ function isDriverProgressActive(dbType: string): boolean {
});
}
function isDriverInstallCancellable(dbType: string): boolean {
return isDriverInstallCancellationTarget(dbType, {
activeOperationId: activeAgentOperationId.value,
cancellableDbType: cancellableAgentInstallDbType.value,
upgradingAll: upgradingAll.value,
progressMap: agentProgressByDbType,
});
}
function driverRequiresJavaRuntime(driver: AgentDriverInfo): boolean {
return driver.requires_java_runtime ?? Boolean(driver.jre);
}
@@ -455,7 +468,7 @@ async function chooseCustomJavaPath() {
}
async function installDriver(dbType: string) {
if (installing.value !== null || upgradingAll.value) {
if (installing.value !== null || preparingUpgradeAll.value || upgradingAll.value) {
queueDriverInstall(dbType);
return;
}
@@ -466,7 +479,6 @@ async function installDriver(dbType: string) {
async function runDriverInstall(dbType: string) {
const label = driverLabel(dbType);
installing.value = dbType;
activeAgentOperationId.value = uuid();
resetAgentInstallProgress();
try {
const managedResult = await installRegisteredManagedJdbcDriver(dbType, jdbcMavenBundles.value, jdbcPluginStatus.value, api);
@@ -486,20 +498,28 @@ async function runDriverInstall(dbType: string) {
toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") }));
return;
}
await api.installAgent(dbType, activeAgentOperationId.value);
const operationId = uuid();
activeAgentOperationId.value = operationId;
cancellableAgentInstallDbType.value = dbType;
await api.installAgent(dbType, operationId);
await refreshAgents();
toast(t("driverStore.driverInstallSuccess", { label }));
} catch (e: any) {
if (isDriverInstallCanceledError(e)) {
toast(t("driverStore.driverInstallCancelled", { label }));
return;
}
toast(t("driverStore.driverInstallFailed", { label, error: backendError(e) }));
} finally {
installing.value = null;
activeAgentOperationId.value = null;
cancellableAgentInstallDbType.value = null;
resetAgentInstallProgress();
}
}
async function runQueuedDriverInstalls() {
if (installing.value !== null || upgradingAll.value) return;
if (installing.value !== null || preparingUpgradeAll.value || upgradingAll.value) return;
const result = takeNextDriverInstallQueue(queuedDriverInstalls.value, canInstallOrUpdateDriver);
queuedDriverInstalls.value = result.queue;
@@ -510,21 +530,29 @@ async function runQueuedDriverInstalls() {
}
async function upgradeAll() {
upgradingAll.value = true;
activeAgentOperationId.value = uuid();
upgradingCompletedCount.value = 0;
queuedDriverInstalls.value = [];
resetAgentInstallProgress();
if (preparingUpgradeAll.value || upgradingAll.value) return;
preparingUpgradeAll.value = true;
try {
const updatableDbTypes = drivers.value.filter((driver) => driver.update_available).map((driver) => driver.db_type);
upgradingTotal.value = updatableDbTypes.length;
const blockers = await api.checkAgentUpdateBlockers(updatableDbTypes);
if (blockers.length > 0) {
toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") }));
return;
}
// Do not expose Cancel until the backend operation has registered its token.
// Otherwise a cancel during this client-side blocker check is silently lost.
upgradingAll.value = true;
activeAgentOperationId.value = uuid();
upgradingCompletedCount.value = 0;
upgradingTotal.value = updatableDbTypes.length;
queuedDriverInstalls.value = [];
resetAgentInstallProgress();
const result = await api.upgradeAllAgents(activeAgentOperationId.value);
await refreshAgents();
if (result.cancelled > 0 && result.failed.length === 0) {
toast(t("driverStore.upgradeAllCancelled", { count: result.cancelled }));
return;
}
if (result.failed.length > 0) {
const failedLabels = result.failed.map((item) => drivers.value.find((driver) => driver.db_type === item.db_type)?.label ?? item.db_type).join(", ");
toast(t("driverStore.upgradeAllPartial", { count: result.upgraded, failed: failedLabels }));
@@ -532,8 +560,13 @@ async function upgradeAll() {
toast(t("driverStore.upgradeAllSuccess", { count: result.upgraded }));
}
} catch (e: any) {
if (isDriverInstallCanceledError(e)) {
toast(t("driverStore.upgradeAllCancelled", { count: 0 }));
return;
}
toast(t("driverStore.upgradeAllFailed", { error: backendError(e) }));
} finally {
preparingUpgradeAll.value = false;
upgradingAll.value = false;
activeAgentOperationId.value = null;
upgradingCompletedCount.value = 0;
@@ -542,6 +575,25 @@ async function upgradeAll() {
}
}
/** Abort a single driver's install, whether started alone or inside a batch. */
async function cancelDriverInstall(dbType: string) {
if (!isDriverInstallCancellable(dbType) || !activeAgentOperationId.value) return;
try {
await api.cancelAgentInstall(dbType, activeAgentOperationId.value);
} catch {
// A cancel that arrives after the download finished is a no-op.
}
}
/** Abort the whole batch upgrade: in-flight downloads stop, queued ones never start. */
async function cancelUpgradeAll() {
try {
await api.cancelAgentUpgradeAll(activeAgentOperationId.value ?? undefined);
} catch {
// The batch may have just finished; treat a failed cancel as a no-op.
}
}
async function uninstallDriver(dbType: string) {
const label = driverLabel(dbType);
try {
@@ -1429,11 +1481,16 @@ watch(driverStoreTab, (tab) => {
<div class="text-sm font-semibold">{{ t("driverStore.updatesAvailableTitle") }} ({{ globalUpdatableDrivers.length }})</div>
<p class="text-xs text-muted-foreground">{{ t("driverStore.updatesAvailableDescription") }}</p>
</div>
<Button size="sm" class="h-7 rounded-md text-xs shrink-0 ml-3" :disabled="installing !== null || upgradingAll || agentImportBusy" @click="upgradeAll">
<Loader2 v-if="upgradingAll" class="h-3 w-3 animate-spin mr-1" />
<Download v-else class="h-3 w-3 mr-1" />
{{ upgradingAll ? t("driverStore.upgradingProgress", { current: upgradingCompletedCount, total: upgradingTotal }) : t("driverStore.upgradeAll") }}
</Button>
<div class="flex shrink-0 items-center gap-2">
<Button size="sm" class="h-7 rounded-md text-xs shrink-0 ml-3" :disabled="installing !== null || preparingUpgradeAll || upgradingAll || agentImportBusy" @click="upgradeAll">
<Loader2 v-if="upgradingAll" class="h-3 w-3 animate-spin mr-1" />
<Download v-else class="h-3 w-3 mr-1" />
{{ upgradingAll ? t("driverStore.upgradingProgress", { current: upgradingCompletedCount, total: upgradingTotal }) : t("driverStore.upgradeAll") }}
</Button>
<Button v-if="upgradingAll" type="button" variant="outline" size="icon-sm" class="h-7 w-7 rounded-md shrink-0 text-muted-foreground hover:text-destructive" :title="t('driverStore.cancelUpgradeAll')" :aria-label="t('driverStore.cancelUpgradeAll')" @click="cancelUpgradeAll">
<X class="h-3.5 w-3.5" />
</Button>
</div>
</div>
<div
v-for="driver in globalUpdatableDrivers"
@@ -1461,14 +1518,24 @@ watch(driverStoreTab, (tab) => {
size="sm"
variant="outline"
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
:disabled="upgradingAll || agentImportBusy"
:disabled="preparingUpgradeAll || upgradingAll || agentImportBusy"
@click="removeQueuedDriverInstall(driver.db_type)"
>
<Clock3 class="h-3 w-3 mr-1" />
{{ t("driverStore.queued") }}
</Button>
<DriverInstallProgressCircle v-else-if="!driver.installed && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.installing'))" />
<Button v-else-if="!driver.installed" size="sm" class="h-7 rounded-md text-xs" :disabled="upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
<DriverInstallProgressCircle v-else-if="!driver.installed && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.installing'))" /><Button
v-if="!driver.installed && isDriverProgressActive(driver.db_type) && isDriverInstallCancellable(driver.db_type)"
type="button"
variant="ghost"
size="icon-sm"
class="h-7 w-7 rounded-md text-muted-foreground hover:text-destructive"
:title="t('driverStore.cancelInstall')"
:aria-label="t('driverStore.cancelInstall')"
@click="cancelDriverInstall(driver.db_type)"
><X class="h-3.5 w-3.5"
/></Button>
<Button v-else-if="!driver.installed && !isDriverProgressActive(driver.db_type)" size="sm" class="h-7 rounded-md text-xs" :disabled="preparingUpgradeAll || upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
<Download class="h-3 w-3 mr-1" />
{{ t("driverStore.install") }}
</Button>
@@ -1478,7 +1545,7 @@ watch(driverStoreTab, (tab) => {
variant="ghost"
class="driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground"
:title="importingDriver === driver.db_type ? t('driverStore.importing') : t('driverStore.importLocalJar')"
:disabled="upgradingAll || installing !== null || agentImportBusy"
:disabled="preparingUpgradeAll || upgradingAll || installing !== null || agentImportBusy"
@click="importDriverFile(driver)"
>
<Loader2 v-if="importingDriver === driver.db_type" class="h-3.5 w-3.5 animate-spin" />
@@ -1490,17 +1557,34 @@ watch(driverStoreTab, (tab) => {
size="sm"
variant="outline"
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
:disabled="upgradingAll || agentImportBusy"
:disabled="preparingUpgradeAll || upgradingAll || agentImportBusy"
@click="removeQueuedDriverInstall(driver.db_type)"
>
<Clock3 class="h-3 w-3 mr-1" />
{{ t("driverStore.queued") }}
</Button>
<DriverInstallProgressCircle v-else-if="driver.installed && driver.update_available && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.updating'))" />
<Button v-else-if="driver.installed && driver.update_available" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
<DriverInstallProgressCircle v-else-if="driver.installed && driver.update_available && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.updating'))" /><Button
v-if="driver.installed && driver.update_available && isDriverProgressActive(driver.db_type) && isDriverInstallCancellable(driver.db_type)"
type="button"
variant="ghost"
size="icon-sm"
class="h-7 w-7 rounded-md text-muted-foreground hover:text-destructive"
:title="t('driverStore.cancelInstall')"
:aria-label="t('driverStore.cancelInstall')"
@click="cancelDriverInstall(driver.db_type)"
><X class="h-3.5 w-3.5"
/></Button>
<Button v-else-if="driver.installed && driver.update_available && !isDriverProgressActive(driver.db_type)" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="preparingUpgradeAll || upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
{{ t("driverStore.update") }}
</Button>
<Button v-if="driver.installed" variant="ghost" size="sm" class="h-7 rounded-md text-xs text-muted-foreground hover:text-destructive" :disabled="installing !== null || upgradingAll || agentImportBusy || isDriverQueued(driver.db_type)" @click="uninstallDriver(driver.db_type)">
<Button
v-if="driver.installed"
variant="ghost"
size="sm"
class="h-7 rounded-md text-xs text-muted-foreground hover:text-destructive"
:disabled="installing !== null || preparingUpgradeAll || upgradingAll || agentImportBusy || isDriverQueued(driver.db_type)"
@click="uninstallDriver(driver.db_type)"
>
{{ t("driverStore.uninstall") }}
</Button>
</div>
@@ -1579,14 +1663,24 @@ watch(driverStoreTab, (tab) => {
size="sm"
variant="outline"
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
:disabled="upgradingAll || agentImportBusy"
:disabled="preparingUpgradeAll || upgradingAll || agentImportBusy"
@click="removeQueuedDriverInstall(driver.db_type)"
>
<Clock3 class="h-3 w-3 mr-1" />
{{ t("driverStore.queued") }}
</Button>
<DriverInstallProgressCircle v-else-if="!driver.installed && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.installing'))" />
<Button v-else-if="!driver.installed" size="sm" class="h-7 rounded-md text-xs" :disabled="upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
<DriverInstallProgressCircle v-else-if="!driver.installed && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.installing'))" /><Button
v-if="!driver.installed && isDriverProgressActive(driver.db_type) && isDriverInstallCancellable(driver.db_type)"
type="button"
variant="ghost"
size="icon-sm"
class="h-7 w-7 rounded-md text-muted-foreground hover:text-destructive"
:title="t('driverStore.cancelInstall')"
:aria-label="t('driverStore.cancelInstall')"
@click="cancelDriverInstall(driver.db_type)"
><X class="h-3.5 w-3.5"
/></Button>
<Button v-else-if="!driver.installed && !isDriverProgressActive(driver.db_type)" size="sm" class="h-7 rounded-md text-xs" :disabled="preparingUpgradeAll || upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
<Download class="h-3 w-3 mr-1" />
{{ t("driverStore.install") }}
</Button>
@@ -1596,7 +1690,7 @@ watch(driverStoreTab, (tab) => {
variant="ghost"
class="driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground"
:title="importingDriver === driver.db_type ? t('driverStore.importing') : t('driverStore.importLocalJar')"
:disabled="upgradingAll || installing !== null || agentImportBusy"
:disabled="preparingUpgradeAll || upgradingAll || installing !== null || agentImportBusy"
@click="importDriverFile(driver)"
>
<Loader2 v-if="importingDriver === driver.db_type" class="h-3.5 w-3.5 animate-spin" />
@@ -1608,14 +1702,24 @@ watch(driverStoreTab, (tab) => {
size="sm"
variant="outline"
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
:disabled="upgradingAll || agentImportBusy"
:disabled="preparingUpgradeAll || upgradingAll || agentImportBusy"
@click="removeQueuedDriverInstall(driver.db_type)"
>
<Clock3 class="h-3 w-3 mr-1" />
{{ t("driverStore.queued") }}
</Button>
<DriverInstallProgressCircle v-else-if="driver.installed && driver.update_available && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.updating'))" />
<Button v-else-if="driver.installed && driver.update_available" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
<DriverInstallProgressCircle v-else-if="driver.installed && driver.update_available && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.updating'))" /><Button
v-if="driver.installed && driver.update_available && isDriverProgressActive(driver.db_type) && isDriverInstallCancellable(driver.db_type)"
type="button"
variant="ghost"
size="icon-sm"
class="h-7 w-7 rounded-md text-muted-foreground hover:text-destructive"
:title="t('driverStore.cancelInstall')"
:aria-label="t('driverStore.cancelInstall')"
@click="cancelDriverInstall(driver.db_type)"
><X class="h-3.5 w-3.5"
/></Button>
<Button v-else-if="driver.installed && driver.update_available && !isDriverProgressActive(driver.db_type)" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="preparingUpgradeAll || upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
{{ t("driverStore.update") }}
</Button>
<Button
@@ -1623,7 +1727,7 @@ watch(driverStoreTab, (tab) => {
variant="ghost"
size="sm"
class="h-7 rounded-md text-xs text-muted-foreground hover:text-destructive"
:disabled="installing !== null || upgradingAll || agentImportBusy || isDriverQueued(driver.db_type)"
:disabled="installing !== null || preparingUpgradeAll || upgradingAll || agentImportBusy || isDriverQueued(driver.db_type)"
@click="uninstallDriver(driver.db_type)"
>
{{ t("driverStore.uninstall") }}
@@ -1663,14 +1767,24 @@ watch(driverStoreTab, (tab) => {
size="sm"
variant="outline"
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
:disabled="upgradingAll || agentImportBusy"
:disabled="preparingUpgradeAll || upgradingAll || agentImportBusy"
@click="removeQueuedDriverInstall(driver.db_type)"
>
<Clock3 class="h-3 w-3 mr-1" />
{{ t("driverStore.queued") }}
</Button>
<DriverInstallProgressCircle v-else-if="!driver.installed && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.installing'))" />
<Button v-else-if="!driver.installed" size="sm" class="h-7 rounded-md text-xs" :disabled="upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
<DriverInstallProgressCircle v-else-if="!driver.installed && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.installing'))" /><Button
v-if="!driver.installed && isDriverProgressActive(driver.db_type) && isDriverInstallCancellable(driver.db_type)"
type="button"
variant="ghost"
size="icon-sm"
class="h-7 w-7 rounded-md text-muted-foreground hover:text-destructive"
:title="t('driverStore.cancelInstall')"
:aria-label="t('driverStore.cancelInstall')"
@click="cancelDriverInstall(driver.db_type)"
><X class="h-3.5 w-3.5"
/></Button>
<Button v-else-if="!driver.installed && !isDriverProgressActive(driver.db_type)" size="sm" class="h-7 rounded-md text-xs" :disabled="preparingUpgradeAll || upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
<Download class="h-3 w-3 mr-1" />
{{ t("driverStore.install") }}
</Button>
@@ -1680,7 +1794,7 @@ watch(driverStoreTab, (tab) => {
variant="ghost"
class="driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground"
:title="importingDriver === driver.db_type ? t('driverStore.importing') : t('driverStore.importLocalJar')"
:disabled="upgradingAll || installing !== null || agentImportBusy"
:disabled="preparingUpgradeAll || upgradingAll || installing !== null || agentImportBusy"
@click="importDriverFile(driver)"
>
<Loader2 v-if="importingDriver === driver.db_type" class="h-3.5 w-3.5 animate-spin" />
@@ -1692,17 +1806,34 @@ watch(driverStoreTab, (tab) => {
size="sm"
variant="outline"
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
:disabled="upgradingAll || agentImportBusy"
:disabled="preparingUpgradeAll || upgradingAll || agentImportBusy"
@click="removeQueuedDriverInstall(driver.db_type)"
>
<Clock3 class="h-3 w-3 mr-1" />
{{ t("driverStore.queued") }}
</Button>
<DriverInstallProgressCircle v-else-if="driver.installed && driver.update_available && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.updating'))" />
<Button v-else-if="driver.installed && driver.update_available" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
<DriverInstallProgressCircle v-else-if="driver.installed && driver.update_available && isDriverProgressActive(driver.db_type)" :percent="getAgentProgressPercent(driver.db_type)" :title="getAgentProgressTitle(driver.db_type, t('driverStore.updating'))" /><Button
v-if="driver.installed && driver.update_available && isDriverProgressActive(driver.db_type) && isDriverInstallCancellable(driver.db_type)"
type="button"
variant="ghost"
size="icon-sm"
class="h-7 w-7 rounded-md text-muted-foreground hover:text-destructive"
:title="t('driverStore.cancelInstall')"
:aria-label="t('driverStore.cancelInstall')"
@click="cancelDriverInstall(driver.db_type)"
><X class="h-3.5 w-3.5"
/></Button>
<Button v-else-if="driver.installed && driver.update_available && !isDriverProgressActive(driver.db_type)" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="preparingUpgradeAll || upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
{{ t("driverStore.update") }}
</Button>
<Button v-if="driver.installed" variant="ghost" size="sm" class="h-7 rounded-md text-xs text-muted-foreground hover:text-destructive" :disabled="installing !== null || upgradingAll || agentImportBusy || isDriverQueued(driver.db_type)" @click="uninstallDriver(driver.db_type)">
<Button
v-if="driver.installed"
variant="ghost"
size="sm"
class="h-7 rounded-md text-xs text-muted-foreground hover:text-destructive"
:disabled="installing !== null || preparingUpgradeAll || upgradingAll || agentImportBusy || isDriverQueued(driver.db_type)"
@click="uninstallDriver(driver.db_type)"
>
{{ t("driverStore.uninstall") }}
</Button>
</div>
@@ -67,7 +67,7 @@ import { loadConnectionPickerView, saveConnectionPickerView, type DbPickerView }
import { normalizeRocketmqNamesrvAddr } from "@/lib/connection/rocketmqNamesrv";
import { normalizeRabbitmqAddresses } from "@/lib/connection/rabbitmqAddresses";
import { detectMqUiAuthKind, isMqAuthKindAllowedForSystem, type MqUiAuthKind } from "@/lib/connection/mqAuth";
import { driverInstallProgressChannel, driverInstallProgressPercent, isDriverInstallProgressForOperation, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
import { driverInstallProgressChannel, driverInstallProgressPercent, isDriverInstallProgressForOperation, requestAgentInstallCancellation, resolveAgentInstallOutcome, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
import { requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityConfig, sqlServerUsesLegacyCompatibility, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
import { normalizeNacosEndpoint, normalizeNacosMetricsUrl, parseNacosManagedNamespaces } from "@/lib/nacos/nacosAdmin";
import { loadReadableNacosNamespaces, nacosNamespaceIdentity, normalizeNacosNamespaceSelection } from "@/lib/nacos/nacosNamespaceVisibility";
@@ -259,6 +259,11 @@ const agentInstallDriverKey = ref("");
const agentInstallLabel = ref("");
const agentInstallProgress = ref<DriverInstallProgress | null>(null);
const agentInstallError = ref("");
const agentInstallCancelError = ref("");
const agentInstallCancelling = ref(false);
/** Set when the user cancels from the modal, so the pending promise's
* "canceled by user" error is treated as a non-failure by its caller. */
const agentInstallCancelRequested = ref(false);
const showConnectionErrorDialog = ref(false);
const connectionErrorRawDetail = ref("");
const connectionErrorDetail = ref("");
@@ -1890,31 +1895,67 @@ async function refreshLocalAgentDrivers(): Promise<AgentDriverInstallState[]> {
return drivers;
}
function beginAgentDriverInstall(driverKey: string, label: string) {
function beginAgentDriverInstall(driverKey: string, label: string): string {
agentInstallOperationId.value = uuid();
agentInstallDriverKey.value = driverKey;
agentInstallLabel.value = label;
agentInstallProgress.value = null;
agentInstallError.value = "";
agentInstallCancelError.value = "";
agentInstallCancelling.value = false;
agentInstallCancelRequested.value = false;
agentInstallRunning.value = true;
showAgentInstallDialog.value = true;
return agentInstallOperationId.value;
}
function finishAgentDriverInstall() {
/**
* Clear the install dialog, unless a newer operation now owns it. Stale
* operation promises (cancelled, then retried before settling) must not
* finish/reset the retry's dialog state.
*/
function finishAgentDriverInstall(operationId?: string | null) {
if (operationId !== undefined && operationId !== null && agentInstallOperationId.value !== operationId) return;
agentInstallOperationId.value = null;
agentInstallRunning.value = false;
agentInstallProgress.value = null;
agentInstallError.value = "";
agentInstallCancelError.value = "";
agentInstallCancelling.value = false;
showAgentInstallDialog.value = false;
}
function failAgentDriverInstall(error: unknown) {
function failAgentDriverInstall(operationId: string | null | undefined, error: unknown) {
if (operationId !== undefined && operationId !== null && agentInstallOperationId.value !== operationId) return;
agentInstallOperationId.value = null;
agentInstallRunning.value = false;
agentInstallCancelError.value = "";
agentInstallCancelling.value = false;
agentInstallError.value = translateBackendError(t, error);
showAgentInstallDialog.value = true;
}
/**
* Abort an in-flight agent driver install from the modal's Cancel button.
* The backend stops the download; the pending `installAgent` promise resolves
* with a "canceled by user" error, which callers treat as a non-failure.
*/
async function cancelActiveAgentInstall() {
const operationId = agentInstallOperationId.value;
if (!agentInstallDriverKey.value || !operationId || agentInstallCancelling.value) return;
agentInstallCancelling.value = true;
agentInstallCancelError.value = "";
const result = await requestAgentInstallCancellation(() => api.cancelAgentInstall(agentInstallDriverKey.value, operationId));
if (agentInstallOperationId.value !== operationId) return;
agentInstallCancelling.value = false;
if (!result.ok) {
agentInstallCancelError.value = translateBackendError(t, result.error);
return;
}
agentInstallCancelRequested.value = true;
finishAgentDriverInstall(operationId);
}
function showConnectionError(message: string) {
connectionErrorRawDetail.value = message;
connectionErrorDetail.value = translateBackendError(t, message);
@@ -1964,14 +2005,35 @@ async function ensureRequiredAgentDriverInstalled(config: ConnectionConfig): Pro
const label = config.driver_label || driverKey;
testResult.value = { ok: true, message: `Installing ${label} driver...` };
beginAgentDriverInstall(driverKey, label);
const operationId = beginAgentDriverInstall(driverKey, label);
try {
await api.installAgent(driverKey, agentInstallOperationId.value ?? undefined);
await api.installAgent(driverKey, operationId);
await refreshLocalAgentDrivers();
finishAgentDriverInstall();
// A stale promise (cancelled then retried) must not close the retry's dialog.
if (agentInstallOperationId.value === operationId) finishAgentDriverInstall(operationId);
} catch (error) {
const outcome = resolveAgentInstallOutcome(
{ ok: false, error },
{
operationId,
currentOperationId: agentInstallOperationId.value,
cancelRequested: agentInstallCancelRequested.value,
},
);
if (outcome.kind === "cancelled") {
// User cancelled: close silently, do not surface a failure.
if (outcome.ownsState) {
testResult.value = null;
finishAgentDriverInstall(operationId);
}
return;
}
if (!outcome.ownsState) {
// A newer operation owns the dialog; leave its state untouched.
return;
}
testResult.value = { ok: false, message: translateBackendError(t, error) };
failAgentDriverInstall(error);
failAgentDriverInstall(operationId, error);
throw error;
}
}
@@ -2019,14 +2081,32 @@ async function installSqlServerLegacyCompatibilityComponentIfNeeded(): Promise<b
if (await api.isAgentInstalled(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY)) return true;
const label = t("connection.sqlServerLegacyCompatibilityComponent");
beginAgentDriverInstall(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY, label);
const operationId = beginAgentDriverInstall(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY, label);
try {
await api.installAgent(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY, agentInstallOperationId.value ?? undefined);
await api.installAgent(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY, operationId);
await refreshLocalAgentDrivers();
finishAgentDriverInstall();
// A stale promise (cancelled then retried) must not close the retry's dialog.
if (agentInstallOperationId.value === operationId) finishAgentDriverInstall(operationId);
} catch (error) {
const outcome = resolveAgentInstallOutcome(
{ ok: false, error },
{
operationId,
currentOperationId: agentInstallOperationId.value,
cancelRequested: agentInstallCancelRequested.value,
},
);
if (outcome.kind === "cancelled") {
// User cancelled the download: the toggle's caller falls back to `auto`.
if (outcome.ownsState) finishAgentDriverInstall(operationId);
return false;
}
if (!outcome.ownsState) {
// A newer operation owns the dialog; leave its state untouched.
return false;
}
testResult.value = { ok: false, message: translateBackendError(t, error) };
failAgentDriverInstall(error);
failAgentDriverInstall(operationId, error);
throw error;
}
return true;
@@ -2042,7 +2122,13 @@ async function setSqlServerDriverMode(mode: "auto" | "legacy") {
}
try {
await installSqlServerLegacyCompatibilityComponentIfNeeded();
const installed = await installSqlServerLegacyCompatibilityComponentIfNeeded();
if (!installed) {
// User cancelled the download: never leave the form in legacy mode
// without the component installed.
setSqlServerLegacyCompatibilityConfig(form.value, false);
return;
}
setSqlServerLegacyCompatibilityConfig(form.value, true);
testResult.value = null;
} catch {
@@ -8535,6 +8621,9 @@ function openExternalUrl(url: string) {
<div class="text-sm font-medium text-destructive">{{ t("connection.driverInstall.fullError") }}</div>
<pre class="max-h-56 min-w-0 max-w-full overflow-x-hidden overflow-y-auto whitespace-pre-wrap break-all [overflow-wrap:anywhere] rounded-md border bg-muted/30 p-3 text-xs leading-5 text-destructive">{{ agentInstallError }}</pre>
</div>
<div v-else-if="agentInstallCancelError" class="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
{{ agentInstallCancelError }}
</div>
</div>
<DialogFooter class="gap-2">
@@ -8542,6 +8631,9 @@ function openExternalUrl(url: string) {
<Copy class="mr-1.5 h-3.5 w-3.5" />
{{ t("connection.copyError") }}
</Button>
<Button v-if="agentInstallRunning && !agentInstallError" variant="outline" :disabled="agentInstallCancelling" @click="cancelActiveAgentInstall">
{{ t("common.cancel") }}
</Button>
<Button :disabled="!canCloseAgentInstallDialog" @click="showAgentInstallDialog = false">
{{ agentInstallError ? t("common.close") : t("connection.driverInstall.installingButton") }}
</Button>
+4
View File
@@ -6445,10 +6445,14 @@ export default {
chooseJavaExecutable: "Choose Java executable",
driverInstallSuccess: "{label} driver installed",
driverInstallFailed: "Failed to install {label} driver: {error}",
driverInstallCancelled: "{label} driver install cancelled",
driverUpdateBlocked: "Close these database connections before updating drivers: {labels}",
upgradeAllSuccess: "{count} driver(s) upgraded",
upgradeAllPartial: "{count} driver(s) upgraded. Skipped: {failed}",
upgradeAllFailed: "Batch upgrade failed: {error}",
upgradeAllCancelled: "Upgrade cancelled ({count} driver(s) aborted)",
cancelInstall: "Cancel install",
cancelUpgradeAll: "Cancel batch upgrade",
driverUninstallSuccess: "{label} driver uninstalled",
driverUninstallFailed: "Failed to uninstall {label} driver: {error}",
chooseOfflineDriverPackage: "Choose offline driver package",
+4
View File
@@ -6267,6 +6267,10 @@ export default withEnglishFallback({
driverStoreDirMigrationFailed: "Error en la migración de controladores: {error}",
driverStoreDirDialogTitle: "Seleccionar {target}",
driverStoreDirSuccess: "{target} cambiado, reiniciando…",
driverInstallCancelled: "Instalación del controlador {label} cancelada",
upgradeAllCancelled: "Actualización masiva cancelada ({count} controladores interrumpidos)",
cancelInstall: "Cancelar instalación",
cancelUpgradeAll: "Cancelar actualización masiva",
},
databaseExport: {
title: "Exportar base de datos",
+4
View File
@@ -6267,6 +6267,10 @@ export default withEnglishFallback({
driverStoreDirMigrationFailed: "Migrazione driver fallita: {error}",
driverStoreDirDialogTitle: "Seleziona {target}",
driverStoreDirSuccess: "{target} cambiato, riavvio in corso…",
driverInstallCancelled: "{label} installazione del driver annullata",
upgradeAllCancelled: "Aggiornamento batch annullato ({count} driver interrotti)",
cancelInstall: "Annulla installazione",
cancelUpgradeAll: "Annulla aggiornamento batch",
},
databaseExport: {
title: "Esporta Database",
+4
View File
@@ -6320,6 +6320,10 @@ export default withEnglishFallback({
driverStoreDirMigrationFailed: "ドライバーストアの移行に失敗しました: {error}",
driverStoreDirDialogTitle: "{target}を選択",
driverStoreDirSuccess: "{target}を変更しました。再起動中…",
driverInstallCancelled: "{label} ドライバーのインストールはキャンセルされました",
upgradeAllCancelled: "一括アップグレードはキャンセルされました({count} 個のドライバーが中止されました)",
cancelInstall: "インストールをキャンセル",
cancelUpgradeAll: "一括アップグレードをキャンセル",
},
databaseExport: {
runInBackground: "バックグラウンドで実行",
+4
View File
@@ -6269,6 +6269,10 @@ export default withEnglishFallback({
driverStoreDirMigrationFailed: "Falha na migração de drivers: {error}",
driverStoreDirDialogTitle: "Selecionar {target}",
driverStoreDirSuccess: "{target} alterado, reiniciando…",
driverInstallCancelled: "{label} instalação do driver cancelada",
upgradeAllCancelled: "Atualização em lote cancelada ({count} drivers interrompidos)",
cancelInstall: "Cancelar instalação",
cancelUpgradeAll: "Cancelar atualização em lote",
},
databaseExport: {
title: "Exportar banco de dados",
+4
View File
@@ -6430,10 +6430,14 @@ export default withEnglishFallback({
chooseJavaExecutable: "选择 Java 可执行文件",
driverInstallSuccess: "{label} 驱动安装成功",
driverInstallFailed: "{label} 驱动安装失败: {error}",
driverInstallCancelled: "{label} 驱动安装已取消",
driverUpdateBlocked: "请先关闭以下数据库连接后再更新驱动: {labels}",
upgradeAllSuccess: "{count} 个驱动升级完成",
upgradeAllPartial: "{count} 个驱动升级完成,以下驱动已跳过: {failed}",
upgradeAllFailed: "批量升级失败: {error}",
upgradeAllCancelled: "批量升级已取消({count} 个驱动中止)",
cancelInstall: "取消安装",
cancelUpgradeAll: "取消批量升级",
driverUninstallSuccess: "{label} 驱动已卸载",
driverUninstallFailed: "{label} 驱动卸载失败: {error}",
chooseOfflineDriverPackage: "选择离线驱动包",
+4
View File
@@ -5591,6 +5591,10 @@ export default withEnglishFallback({
jdbcSourceMaven: "Maven",
jdbcSourceManual: "手動",
noMatchingDrivers: "沒有相符的驅動程式。",
driverInstallCancelled: "{label} 驅動安裝已取消",
upgradeAllCancelled: "批次升級已取消({count} 個驅動中止)",
cancelInstall: "取消安裝",
cancelUpgradeAll: "取消批次升級",
},
databaseExport: {
title: "匯出資料庫",
+2
View File
@@ -106,6 +106,8 @@ export async function upgradeAllAgents(operationId?: string) {
const backend = await getBackend();
return backend.upgradeAllAgents(useSettingsStore().editorSettings.updateDownloadSource, operationId);
}
export const cancelAgentInstall = forward("cancelAgentInstall");
export const cancelAgentUpgradeAll = forward("cancelAgentUpgradeAll");
export const checkAgentUpdateBlockers = forward("checkAgentUpdateBlockers");
export const uninstallAgent = forward("uninstallAgent");
export const getAgentJavaRuntimeConfig = forward("getAgentJavaRuntimeConfig");
+8
View File
@@ -582,6 +582,14 @@ export async function upgradeAllAgents(_source?: UpdateDownloadSource, operation
return post("/api/agents/upgrade-all", { operationId });
}
export async function cancelAgentInstall(dbType: string, operationId?: string): Promise<void> {
await post("/api/agents/cancel-install", { dbType, operationId });
}
export async function cancelAgentUpgradeAll(operationId?: string): Promise<void> {
await post("/api/agents/cancel-upgrade-all", { operationId });
}
export async function checkAgentUpdateBlockers(dbTypes: string[]): Promise<AgentUpdateBlocker[]> {
return post("/api/agents/update-blockers", { dbTypes });
}
+10
View File
@@ -118,6 +118,8 @@ export interface AgentDriverUpdateIssue {
export interface UpgradeAllAgentDriversResult {
upgraded: number;
/** Drivers whose install was aborted by a user cancel (single-driver or batch). */
cancelled: number;
failed: AgentDriverUpdateIssue[];
}
@@ -1915,6 +1917,14 @@ export async function upgradeAllAgents(source?: UpdateDownloadSource, operationI
return invoke("upgrade_all_agents", { source, operationId });
}
export async function cancelAgentInstall(dbType: string, operationId?: string): Promise<void> {
return invoke("cancel_agent_install", { dbType, operationId });
}
export async function cancelAgentUpgradeAll(operationId?: string): Promise<void> {
return invoke("cancel_agent_upgrade_all", { operationId });
}
export async function checkAgentUpdateBlockers(dbTypes: string[]): Promise<AgentUpdateBlocker[]> {
return invoke("check_agent_update_blockers", { dbTypes });
}
@@ -16,16 +16,74 @@ export interface DriverInstallProgressTargetState {
progressMap: Record<string, DriverInstallProgress | null | undefined>;
}
export interface DriverInstallCancellationTargetState {
activeOperationId: string | null;
cancellableDbType: string | null;
upgradingAll: boolean;
progressMap: Record<string, DriverInstallProgress | null | undefined>;
}
export type DriverInstallProgressChannel = "agent" | "jdbc-plugin";
const AGENT_PROGRESS_STEPS = new Set(["driver", "jre", "jre-extract", "all-done"]);
/** Substring the backend uses for a user-cancelled driver install/upgrade. */
const AGENT_DOWNLOAD_CANCELED_ERROR = "Agent download canceled by user.";
/**
* True when a driver-install error is a user cancel rather than a genuine
* failure. Callers use this to close install modals silently and to avoid
* reporting a cancelled driver/upgrade as failed.
*/
export function isDriverInstallCanceledError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String((error as { message?: string } | null)?.message ?? error ?? "");
return message.includes(AGENT_DOWNLOAD_CANCELED_ERROR);
}
export function isDriverInstallProgressForOperation(progress: DriverInstallProgress, operationId: string | null): boolean {
// Keep accepting legacy unscoped events, while preventing another active
// install's terminal/progress event from mutating this dialog.
return !progress.operation_id || progress.operation_id === operationId;
}
/** How an agent install promise settled relative to the dialog's state. */
export interface AgentInstallOperationContext {
/** Operation id captured when the install promise was created. */
operationId: string;
/** Operation id the dialog is currently tracking (null once finished). */
currentOperationId: string | null;
/** Whether the user pressed Cancel on the dialog's tracked operation. */
cancelRequested: boolean;
}
export type AgentInstallOutcome = { kind: "succeeded"; ownsState: boolean } | { kind: "cancelled"; ownsState: boolean } | { kind: "failed"; ownsState: boolean };
export type AgentInstallCancelRequestResult = { ok: true } | { ok: false; error: unknown };
/**
* Classify the settlement of an agent install operation promise. When the
* dialog no longer tracks the promise's operation id (the user cancelled and
* immediately retried, so a newer operation owns the state), `ownsState` is
* false and the stale promise must not finish/fail the new operation's UI.
*/
export function resolveAgentInstallOutcome(result: { ok: true } | { ok: false; error: unknown }, context: AgentInstallOperationContext): AgentInstallOutcome {
const ownsState = context.currentOperationId === context.operationId;
if (result.ok) return { kind: "succeeded", ownsState };
if (isDriverInstallCanceledError(result.error) || context.cancelRequested) {
return { kind: "cancelled", ownsState };
}
return { kind: "failed", ownsState };
}
export async function requestAgentInstallCancellation(request: () => Promise<void>): Promise<AgentInstallCancelRequestResult> {
try {
await request();
return { ok: true };
} catch (error) {
return { ok: false, error };
}
}
export function driverInstallProgressChannel(progress: DriverInstallProgress): DriverInstallProgressChannel | null {
if (progress.step === "jdbc-plugin" || progress.step === "jdbc-plugin-extract") return "jdbc-plugin";
if (progress.db_type || AGENT_PROGRESS_STEPS.has(progress.step)) return "agent";
@@ -73,6 +131,13 @@ export function isDriverInstallProgressTarget(dbType: string, state: DriverInsta
return progress !== null && progress !== undefined;
}
export function isDriverInstallCancellationTarget(dbType: string, state: DriverInstallCancellationTargetState): boolean {
if (!state.activeOperationId) return false;
if (!state.upgradingAll) return state.cancellableDbType === dbType;
const progress = state.progressMap[dbType];
return progress !== null && progress !== undefined;
}
export function addDriverInstallQueue(queue: string[], dbType: string, activeDbType: string | null): string[] {
if (activeDbType === dbType || queue.includes(dbType)) return queue;
return [...queue, dbType];