feat(drivers): queue agent installs

This commit is contained in:
t8y2 committed 2026-05-18 17:46:59 +08:00
1 parent cf86d7f3b4
commit 51ab706c0e
4 files changed
+254 -38

No files matched your search

@@ -0,0 +1,35 @@
<script setup lang="ts">
const props = defineProps<{
percent: number | null;
title: string;
}>();
</script>
<template>
<span
class="relative flex h-8 w-8 shrink-0 items-center justify-center rounded-full"
role="status"
:aria-label="title"
:title="title"
>
<svg class="absolute inset-0 h-8 w-8 -rotate-90" viewBox="0 0 32 32" aria-hidden="true">
<circle class="text-green-600/20" cx="16" cy="16" r="13" fill="none" stroke="currentColor" stroke-width="3" />
<circle
class="text-green-600 transition-[stroke-dashoffset] duration-200"
cx="16"
cy="16"
r="13"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
pathLength="100"
stroke-dasharray="100"
:stroke-dashoffset="100 - (props.percent ?? 0)"
/>
</svg>
<span class="relative z-10 font-mono text-[10px] font-semibold leading-none text-green-700 tabular-nums">
{{ props.percent ?? "..." }}
</span>
</span>
</template>
@@ -4,18 +4,27 @@ import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import type { UnlistenFn } from "@tauri-apps/api/event";
import { useI18n } from "vue-i18n";
import { FolderOpen, Trash2, Download, RotateCcw, Loader2, RefreshCw, Check } from "lucide-vue-next";
import { FolderOpen, Trash2, Download, RotateCcw, Loader2, RefreshCw, Check, Clock3 } from "lucide-vue-next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import DriverInstallProgressCircle from "@/components/config/DriverInstallProgressCircle.vue";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import { useToast } from "@/composables/useToast";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { countAvailableAgentDriverUpdates } from "@/lib/agentDriverUpdateBadge";
import type { JdbcDriverInfo, JdbcPluginStatus } from "@/types/database";
import * as api from "@/lib/api";
import {
addDriverInstallQueue,
driverInstallProgressPercent,
isDriverInstallProgressTarget,
removeDriverInstallQueue,
takeNextDriverInstallQueue,
type DriverInstallProgress,
} from "@/lib/driverInstallProgressUi";
const { t } = useI18n();
const { toast } = useToast();
@@ -39,12 +48,6 @@ interface AgentDriverInfo {
jre_installed: boolean;
}
interface InstallProgress {
step: string;
downloaded?: number;
total?: number;
}
type JavaRuntimeMode = "managed" | "system" | "custom";
interface JavaRuntimeConfig {
@@ -58,9 +61,10 @@ const upgradingAll = ref(false);
const upgradingCurrent = ref("");
const upgradingIndex = ref(0);
const upgradingTotal = ref(0);
const queuedDriverInstalls = ref<string[]>([]);
const reinstallingJre = ref<string | null>(null);
const refreshing = ref(false);
const progress = ref<InstallProgress | null>(null);
const progress = ref<DriverInstallProgress | null>(null);
const javaRuntimeConfig = ref<JavaRuntimeConfig>({ mode: "managed", custom_java_path: null });
const customJavaPath = ref("");
const savingJavaRuntime = ref(false);
@@ -95,11 +99,7 @@ const progressText = computed(() => {
return `${prefix}${label} ${dl} / ${total} (${pct}%)`;
});
const progressPercent = computed(() => {
const p = progress.value;
if (!p || !p.total) return 0;
return Math.round(((p.downloaded ?? 0) / p.total) * 100);
});
const progressNumber = computed(() => driverInstallProgressPercent(progress.value));
const updatableCount = computed(() => drivers.value.filter((d) => d.update_available).length);
@@ -108,6 +108,35 @@ function updateAgentDrivers(nextDrivers: AgentDriverInfo[]) {
emit("update-count-change", countAvailableAgentDriverUpdates(nextDrivers));
}
function isDriverProgressActive(dbType: string): boolean {
return isDriverInstallProgressTarget(dbType, {
installing: installing.value,
upgradingAll: upgradingAll.value,
progress: progress.value,
});
}
function progressTitle(fallback: string): string {
return progressText.value || fallback;
}
function isDriverQueued(dbType: string): boolean {
return queuedDriverInstalls.value.includes(dbType);
}
function canInstallOrUpdateDriver(dbType: string): boolean {
const driver = drivers.value.find((d) => d.db_type === dbType);
return Boolean(driver && (!driver.installed || driver.update_available));
}
function queueDriverInstall(dbType: string) {
queuedDriverInstalls.value = addDriverInstallQueue(queuedDriverInstalls.value, dbType, installing.value);
}
function removeQueuedDriverInstall(dbType: string) {
queuedDriverInstalls.value = removeDriverInstallQueue(queuedDriverInstalls.value, dbType);
}
async function refreshAgents() {
updateAgentDrivers(await invoke<AgentDriverInfo[]>("list_installed_agents"));
}
@@ -166,6 +195,15 @@ async function chooseCustomJavaPath() {
}
async function installDriver(dbType: string) {
if (installing.value !== null || upgradingAll.value) {
queueDriverInstall(dbType);
return;
}
await runDriverInstall(dbType);
await runQueuedDriverInstalls();
}
async function runDriverInstall(dbType: string) {
const label = drivers.value.find((d) => d.db_type === dbType)?.label ?? dbType;
installing.value = dbType;
progress.value = null;
@@ -181,8 +219,20 @@ async function installDriver(dbType: string) {
}
}
async function runQueuedDriverInstalls() {
if (installing.value !== null || upgradingAll.value) return;
const result = takeNextDriverInstallQueue(queuedDriverInstalls.value, canInstallOrUpdateDriver);
queuedDriverInstalls.value = result.queue;
if (!result.next) return;
await runDriverInstall(result.next);
await runQueuedDriverInstalls();
}
async function upgradeAll() {
upgradingAll.value = true;
queuedDriverInstalls.value = [];
progress.value = null;
try {
const count = await invoke<number>("upgrade_all_agents");
@@ -380,7 +430,7 @@ onMounted(async () => {
updateAgentDrivers(result);
});
unlisten = await listen<InstallProgress>("agent-install-progress", (event) => {
unlisten = await listen<DriverInstallProgress>("agent-install-progress", (event) => {
const payload = event.payload as any;
if (payload.step === "done" || payload.step === "all-done") {
progress.value = null;
@@ -476,27 +526,34 @@ onUnmounted(() => {
<div class="flex shrink-0 items-center gap-3">
<Check v-if="jre.installed" class="h-4 w-4 text-green-600" />
<span v-else class="text-xs text-muted-foreground">未安装</span>
<DriverInstallProgressCircle
v-if="reinstallingJre === jre.key"
:percent="progressNumber"
:title="progressTitle(jre.installed ? '重装中' : '安装中')"
/>
<Button
v-if="!jre.installed"
v-else-if="!jre.installed"
type="button"
variant="default"
size="sm"
class="h-8 text-xs"
:disabled="reinstallingJre !== null || installing !== null"
@click="reinstallJre(jre.key)"
>
<Download class="h-3.5 w-3.5 mr-1" />
{{ reinstallingJre === jre.key ? "安装中..." : "安装" }}
安装
</Button>
<Button
v-if="jre.installed"
v-else-if="jre.installed"
type="button"
variant="outline"
size="sm"
class="h-8 text-xs"
:disabled="reinstallingJre !== null || installing !== null"
@click="reinstallJre(jre.key)"
>
<RotateCcw class="h-3.5 w-3.5 mr-1" />
{{ reinstallingJre === jre.key ? "重装中..." : "重新安装" }}
重新安装
</Button>
<Button
v-if="jre.installed"
@@ -517,17 +574,6 @@ onUnmounted(() => {
<p class="text-xs text-muted-foreground mt-0.5">首次安装驱动时自动下载</p>
</div>
<!-- Progress bar -->
<div v-if="progress" class="space-y-1.5 px-1">
<div class="text-xs text-muted-foreground">{{ progressText }}</div>
<div class="h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-primary transition-all duration-200"
:style="{ width: `${progressPercent}%` }"
/>
</div>
</div>
<!-- Driver List -->
<div v-if="drivers.length === 0" class="py-12 text-center text-sm text-muted-foreground">加载中...</div>
<div v-else class="rounded-md border divide-y">
@@ -587,33 +633,64 @@ onUnmounted(() => {
</div>
<div class="flex shrink-0 items-center gap-2">
<Button
v-if="!driver.installed"
v-if="!driver.installed && isDriverQueued(driver.db_type)"
size="sm"
variant="outline"
class="h-7 border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
:disabled="upgradingAll"
@click="removeQueuedDriverInstall(driver.db_type)"
>
<Clock3 class="h-3 w-3 mr-1" />
排队中
</Button>
<DriverInstallProgressCircle
v-else-if="!driver.installed && isDriverProgressActive(driver.db_type)"
:percent="progressNumber"
:title="progressTitle('安装中')"
/>
<Button
v-else-if="!driver.installed"
size="sm"
class="h-7 text-xs"
:disabled="installing !== null || upgradingAll"
:disabled="upgradingAll"
@click="installDriver(driver.db_type)"
>
<Loader2 v-if="installing === driver.db_type" class="h-3 w-3 animate-spin mr-1" />
<Download v-else class="h-3 w-3 mr-1" />
{{ installing === driver.db_type ? "安装中..." : "安装" }}
<Download class="h-3 w-3 mr-1" />
安装
</Button>
<template v-else>
<Check class="h-4 w-4 text-green-600" />
<Button
v-if="driver.update_available"
v-if="driver.update_available && isDriverQueued(driver.db_type)"
size="sm"
variant="outline"
class="h-7 border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
:disabled="upgradingAll"
@click="removeQueuedDriverInstall(driver.db_type)"
>
<Clock3 class="h-3 w-3 mr-1" />
排队中
</Button>
<DriverInstallProgressCircle
v-else-if="driver.update_available && isDriverProgressActive(driver.db_type)"
:percent="progressNumber"
:title="progressTitle('更新中')"
/>
<Button
v-else-if="driver.update_available"
size="sm"
variant="outline"
class="h-7 text-xs"
:disabled="installing !== null || upgradingAll"
:disabled="upgradingAll"
@click="installDriver(driver.db_type)"
>
{{ installing === driver.db_type ? "更新中..." : "更新" }}
更新
</Button>
<Button
variant="ghost"
size="sm"
class="h-7 text-xs text-muted-foreground hover:text-destructive"
:disabled="upgradingAll"
:disabled="installing !== null || upgradingAll || isDriverQueued(driver.db_type)"
@click="uninstallDriver(driver.db_type)"
>
卸载
@@ -0,0 +1,46 @@
export interface DriverInstallProgress {
step: string;
downloaded?: number;
total?: number;
db_type?: string;
}
export interface DriverInstallProgressTargetState {
installing: string | null;
upgradingAll: boolean;
progress: DriverInstallProgress | null;
}
export function driverInstallProgressPercent(progress: DriverInstallProgress | null): number | null {
if (!progress?.total || progress.total <= 0) return null;
const percent = Math.round(((progress.downloaded ?? 0) / progress.total) * 100);
return Math.min(100, Math.max(0, percent));
}
export function isDriverInstallProgressTarget(dbType: string, state: DriverInstallProgressTargetState): boolean {
if (state.installing === dbType) return true;
return state.upgradingAll && state.progress?.db_type === dbType;
}
export function addDriverInstallQueue(queue: string[], dbType: string, activeDbType: string | null): string[] {
if (activeDbType === dbType || queue.includes(dbType)) return queue;
return [...queue, dbType];
}
export function removeDriverInstallQueue(queue: string[], dbType: string): string[] {
return queue.filter((queuedDbType) => queuedDbType !== dbType);
}
export function takeNextDriverInstallQueue(
queue: string[],
isInstallable: (dbType: string) => boolean,
): { next: string | null; queue: string[] } {
const remaining = [...queue];
while (remaining.length > 0) {
const next = remaining.shift() ?? null;
if (next && isInstallable(next)) {
return { next, queue: remaining };
}
}
return { next: null, queue: [] };
}