feat(driver-store): filter built-in drivers by install status

This commit is contained in:
onenewcode
2026-09-16 20:16:29 +08:00
committed by GitHub
parent 888f37c3c8
commit a57906ed7e
19 changed files with 727 additions and 320 deletions
@@ -0,0 +1,140 @@
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { Check, Clock3, Download, FileUp, Loader2, X } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import DriverInstallProgressCircle from "@/components/config/DriverInstallProgressCircle.vue";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import type { AgentDriverInfo } from "@/lib/backend/api";
const props = withDefaults(
defineProps<{
driver: AgentDriverInfo;
showCategoryBadge?: boolean;
categoryLabel?: string;
highlighted?: boolean;
sizeLabel: string;
requiresJavaRuntime: boolean;
queued: boolean;
progressActive: boolean;
progressCancellable: boolean;
progressPercent: number | null;
progressText: string;
managedJdbc: boolean;
importing: boolean;
packageBusy: boolean;
preparingUpgradeAll: boolean;
upgradingAll: boolean;
installing: boolean;
}>(),
{
showCategoryBadge: false,
categoryLabel: "",
highlighted: false,
},
);
const emit = defineEmits<{
install: [];
uninstall: [];
importFile: [];
cancelInstall: [];
removeQueue: [];
}>();
const { t } = useI18n();
const progressTitle = computed(() => props.progressText || t(props.driver.installed && props.driver.update_available ? "driverStore.updating" : "driverStore.installing"));
const queueOrUpgradeBusy = computed(() => props.preparingUpgradeAll || props.upgradingAll || props.packageBusy);
const uninstallBusy = computed(() => props.installing || props.preparingUpgradeAll || props.upgradingAll || props.packageBusy || props.queued);
</script>
<template>
<div
:data-driver-store-focus="`driver:${driver.db_type}`"
class="driver-store-agent-row flex items-center gap-3 px-4 py-2 transition hover:bg-muted/30"
:class="{
'driver-store-focus-highlight': highlighted,
'driver-store-agent-row--installed': driver.installed,
}"
>
<span class="relative flex h-8 w-8 items-center justify-center rounded-md bg-muted/60 shrink-0">
<DatabaseIcon :db-type="driver.db_type" class="h-4 w-4" />
<span v-if="driver.installed" class="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-background ring-1 ring-background" aria-hidden="true">
<Check class="h-2.5 w-2.5 text-green-600" />
</span>
</span>
<div class="driver-store-agent-name min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-1.5">
<div class="truncate text-sm font-medium">{{ driver.label }}</div>
<span v-if="driver.installed" class="driver-store-installed-badge shrink-0 rounded-full bg-green-500/15 px-2 py-0.5 text-[11px] font-medium text-green-700 dark:text-green-400">{{ t("driverStore.installedBadge") }}</span>
</div>
</div>
<span v-if="showCategoryBadge && categoryLabel" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">{{ categoryLabel }}</span>
<div class="driver-store-agent-meta flex shrink-0 items-center gap-1.5">
<span v-if="requiresJavaRuntime && driver.jre" class="rounded-full px-2 py-0.5 text-[11px]" :class="driver.jre !== '21' ? 'bg-blue-500/10 text-blue-600' : 'bg-muted text-muted-foreground'">JRE {{ driver.jre }}</span>
<span v-if="driver.installed" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">v{{ driver.installed_version }}</span>
<span v-if="driver.installed && driver.update_available" class="rounded-full bg-amber-500/15 px-2 py-0.5 text-[11px] text-amber-600">→ v{{ driver.version }}</span>
<span v-if="!driver.installed && driver.version" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">v{{ driver.version }}</span>
<span v-if="sizeLabel" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">{{ sizeLabel }}</span>
</div>
<div class="driver-store-agent-actions flex shrink-0 items-center gap-2">
<Button v-if="!driver.installed && queued" 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="queueOrUpgradeBusy" @click="emit('removeQueue')">
<Clock3 class="h-3 w-3 mr-1" />
{{ t("driverStore.queued") }}
</Button>
<DriverInstallProgressCircle v-else-if="!driver.installed && progressActive" :percent="progressPercent" :title="progressTitle" />
<Button
v-if="!driver.installed && progressActive && progressCancellable"
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="emit('cancelInstall')"
>
<X class="h-3.5 w-3.5" />
</Button>
<Button v-else-if="!driver.installed && !progressActive && !queued" size="sm" class="h-7 rounded-md text-xs" :disabled="queueOrUpgradeBusy" @click="emit('install')">
<Download class="h-3 w-3 mr-1" />
{{ t("driverStore.install") }}
</Button>
<Button
v-if="!driver.installed && !managedJdbc && !progressActive && !queued"
size="sm"
variant="ghost"
class="driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground"
:title="importing ? t('driverStore.importing') : t('driverStore.importLocalJar')"
:disabled="preparingUpgradeAll || upgradingAll || installing || packageBusy"
@click="emit('importFile')"
>
<Loader2 v-if="importing" class="h-3.5 w-3.5 animate-spin" />
<FileUp v-else class="h-3.5 w-3.5" />
</Button>
<Button v-if="driver.installed && driver.update_available && queued" 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="queueOrUpgradeBusy" @click="emit('removeQueue')">
<Clock3 class="h-3 w-3 mr-1" />
{{ t("driverStore.queued") }}
</Button>
<DriverInstallProgressCircle v-else-if="driver.installed && driver.update_available && progressActive" :percent="progressPercent" :title="progressTitle" />
<Button
v-if="driver.installed && driver.update_available && progressActive && progressCancellable"
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="emit('cancelInstall')"
>
<X class="h-3.5 w-3.5" />
</Button>
<Button v-else-if="driver.installed && driver.update_available && !progressActive && !queued" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="queueOrUpgradeBusy" @click="emit('install')">
{{ 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="uninstallBusy" @click="emit('uninstall')">
{{ t("driverStore.uninstall") }}
</Button>
</div>
</div>
</template>
@@ -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, FileArchive, FileUp, X } from "@lucide/vue";
import { Activity, ExternalLink, Cpu, FolderOpen, FolderSync, MemoryStick, Search, Square, Trash2, Download, RotateCcw, Loader2, RefreshCw, Check, FileArchive, FileUp, X } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -10,8 +10,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import DriverInstallProgressCircle from "@/components/config/DriverInstallProgressCircle.vue";
import DriverStoreAgentRow from "@/components/config/DriverStoreAgentRow.vue";
import AgentOfflineExportDialog from "@/components/config/AgentOfflineExportDialog.vue";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import { useToast } from "@/composables/useToast";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { uuid } from "@/lib/common/utils";
@@ -34,12 +34,12 @@ import {
type DriverInstallProgress,
} from "@/lib/connection/driverInstallProgressUi";
import { installRegisteredManagedJdbcDriver, isManagedJdbcDriver, managedJdbcDriverRows, uninstallRegisteredManagedJdbcDriver } from "@/lib/database/managedJdbcDrivers";
import type { DriverStoreFocus, DriverStoreTab } from "@/lib/connection/agentDriverInstallHint";
import { driverStoreFocusElementKey, driverStoreFocusRowIsRenderable, shouldApplyDriverStoreFocus, type DriverStoreFocus, type DriverStoreTab } from "@/lib/connection/agentDriverInstallHint";
import { isOfflineDriverPackage, webDriverImportAccept } from "@/lib/driverStore/driverImportSelection";
import { translateBackendError } from "@/i18n/backend-errors";
import { runAgentOfflineExportAction } from "@/lib/driverStore/agentOfflineExportFlow";
import { DRIVER_CATEGORIES, getCategoryForAgentDriver, assertAgentDriverCategoriesComplete } from "@/lib/connection/driver-category-definitions";
import { selectUpdatableDrivers, selectStableDrivers, hasAnyUpdatableDriverMatching } from "@/lib/connection/driverListFilter";
import { hasAnyUpdatableDriverMatching, countInstalledDrivers, countAvailableDrivers, partitionDriversByInstallStatus, upgradeAllDriverTypes, upgradeAllMatchesFullUpdateSet, type DriverInstallStatusFilter } from "@/lib/connection/driverListFilter";
const { t } = useI18n();
const { toast } = useToast();
@@ -190,6 +190,7 @@ async function applyDriverStoreDir(kind: DriverStoreDirKind, newDir: string | nu
const drivers = ref<AgentDriverInfo[]>([]);
const agentDriverSearch = ref("");
const selectedDriverCategory = ref<string>("all");
const driverStatusFilter = ref<DriverInstallStatusFilter>("all");
const installing = ref<string | null>(null);
const upgradingAll = ref(false);
const preparingUpgradeAll = ref(false);
@@ -256,10 +257,6 @@ function getJdbcPluginProgressTitle(fallback: string): string {
return jdbcPluginProgressText.value || fallback;
}
function getAgentProgressTitle(dbType: string, fallback: string): string {
return getAgentProgressText(dbType) || formatProgressText(agentProgressByDbType[dbType]) || fallback;
}
const jdbcPluginProgressText = computed(() => formatProgressText(jdbcPluginProgress.value));
function getAgentProgressPercent(dbType: string): number | null {
@@ -541,9 +538,11 @@ async function runQueuedDriverInstalls() {
async function upgradeAll() {
if (agentPackageBusy.value || preparingUpgradeAll.value || upgradingAll.value) return;
if (!upgradeAllMatchesFullUpdateSet(builtinDriverRows.value, driverStatusFilter.value)) return;
preparingUpgradeAll.value = true;
try {
const updatableDbTypes = drivers.value.filter((driver) => driver.update_available).map((driver) => driver.db_type);
const updatableDbTypes = upgradeAllDriverTypes(builtinDriverRows.value, driverStatusFilter.value);
if (updatableDbTypes.length === 0) return;
const blockers = await api.checkAgentUpdateBlockers(updatableDbTypes);
if (blockers.length > 0) {
toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") }));
@@ -855,6 +854,28 @@ function formatSize(bytes: number): string {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
function agentRowProps(driver: AgentDriverInfo, options: { showCategoryBadge?: boolean } = {}) {
return {
driver,
highlighted: highlightedFocusKey.value === `driver:${driver.db_type}`,
sizeLabel: formatSize(driver.size),
requiresJavaRuntime: driverRequiresJavaRuntime(driver),
queued: isDriverQueued(driver.db_type),
progressActive: isDriverProgressActive(driver.db_type),
progressCancellable: isDriverInstallCancellable(driver.db_type),
progressPercent: getAgentProgressPercent(driver.db_type),
progressText: getAgentProgressText(driver.db_type),
managedJdbc: isManagedJdbcBuiltinDriver(driver.db_type),
importing: importingDriver.value === driver.db_type,
packageBusy: agentPackageBusy.value,
preparingUpgradeAll: preparingUpgradeAll.value,
upgradingAll: upgradingAll.value,
installing: installing.value !== null,
showCategoryBadge: options.showCategoryBadge === true,
categoryLabel: options.showCategoryBadge ? driverCategoryLabel(driver.db_type) : "",
};
}
// ──────────── JDBC drivers ────────────
const jdbcDrivers = ref<JdbcDriverInfo[]>([]);
@@ -927,8 +948,22 @@ function driverCategoryLabel(dbType: string): string {
return catDef ? t(catDef.titleKey) : t("driverStore.driverCategoryAll");
}
const driverStatusCounts = computed(() => ({
all: builtinDriverRows.value.length,
installed: countInstalledDrivers(builtinDriverRows.value),
available: countAvailableDrivers(builtinDriverRows.value),
}));
const driverStatusFilterOptions = computed(() => [
{ value: "all" as const, label: t("driverStore.statusFilterAll", { count: driverStatusCounts.value.all }) },
{ value: "installed" as const, label: t("driverStore.statusFilterInstalled", { count: driverStatusCounts.value.installed }) },
{ value: "available" as const, label: t("driverStore.statusFilterAvailable", { count: driverStatusCounts.value.available }) },
]);
const partitionedDrivers = computed(() => partitionDriversByInstallStatus(builtinDriverRows.value, driverStatusFilter.value));
// Search only stable drivers — updatable drivers already appear in the global update section.
const stableBuiltinDrivers = computed(() => selectStableDrivers(builtinDriverRows.value));
const stableBuiltinDrivers = computed(() => partitionedDrivers.value.stable);
// Filter stable drivers through search (or pass all through if no search)
const searchedDrivers = computed(() => {
@@ -962,8 +997,10 @@ const categoryFilteredDrivers = computed(() => {
return searchedDrivers.value.filter((driver) => getCategoryForAgentDriver(driver.db_type) === selectedDriverCategory.value);
});
// Global updatable drivers — always shown above category navigation, regardless of filter.
const globalUpdatableDrivers = computed(() => selectUpdatableDrivers(builtinDriverRows.value));
// Global updatable drivers — status-filtered banner. Hidden on "available" so stale
// uninstalled update_available rows stay in the list instead of vanishing.
const globalUpdatableDrivers = computed(() => partitionedDrivers.value.updatable);
const canBatchUpgradeAll = computed(() => upgradingAll.value || upgradeAllMatchesFullUpdateSet(builtinDriverRows.value, driverStatusFilter.value));
// Whether any global updatable driver matches the current search query or
// selected category. Used to decide whether the empty‑state message should be
@@ -976,11 +1013,15 @@ const hasMatchingUpdatableDrivers = computed(() =>
selectedCategory: selectedDriverCategory.value,
driverMatchesSearch: (driver, query) => [driver.label, driver.db_type, driver.version, driver.installed_version, driverRequiresJavaRuntime(driver) ? driver.jre : "", driverCategoryLabel(driver.db_type)].filter(Boolean).join(" ").toLowerCase().includes(query),
driverCategory: (driver) => getCategoryForAgentDriver(driver.db_type),
installStatus: driverStatusFilter.value,
}),
);
// Category-filtered drivers, excluding those already shown in the global update banner.
const categoryStableDrivers = computed(() => selectStableDrivers(categoryFilteredDrivers.value));
const showInstalledEmptyState = computed(() => driverStatusFilter.value === "installed" && driverStatusCounts.value.installed === 0 && !isDriverSearchActive.value);
const showAvailableEmptyState = computed(() => driverStatusFilter.value === "available" && driverStatusCounts.value.available === 0 && !isDriverSearchActive.value);
// Category-filtered drivers. Already partitioned; do not re-apply selectStableDrivers.
const categoryStableDrivers = computed(() => categoryFilteredDrivers.value);
// Category selection handler
function selectDriverCategory(key: string) {
@@ -990,24 +1031,28 @@ function selectDriverCategory(key: string) {
}
const highlightedFocusKey = ref<string | null>(null);
const lastAppliedFocusKey = ref<string | null>(null);
let focusHighlightTimer: ReturnType<typeof setTimeout> | undefined;
function focusElementKey(focus: DriverStoreFocus): string {
return focus.target === "driver" ? `driver:${focus.driver ?? ""}` : "jre";
}
watch(
[() => props.focusTarget, builtinDriverRows],
async ([focus]) => {
if (!focus || focus.target === "tab") return;
async ([focus], previous) => {
if (!focus || focus.target === "tab") {
lastAppliedFocusKey.value = null;
return;
}
if (!driverStoreFocusRowIsRenderable(focus, drivers.value.length, builtinDriverRows.value)) return;
const key = driverStoreFocusElementKey(focus);
const previousFocus = previous?.[0];
const focusChanged = previousFocus !== focus;
if (!shouldApplyDriverStoreFocus(lastAppliedFocusKey.value, key, focusChanged)) return;
lastAppliedFocusKey.value = key;
driverStoreTab.value = "agent";
if (focus.target === "driver") {
// Wait until the requested driver row is loaded before scrolling to it.
if (!focus.driver || !builtinDriverRows.value.some((driver) => driver.db_type === focus.driver)) return;
agentDriverSearch.value = "";
selectedDriverCategory.value = "all";
driverStatusFilter.value = "all";
}
const key = focusElementKey(focus);
highlightedFocusKey.value = key;
await nextTick();
document.querySelector(`[data-driver-store-focus="${CSS.escape(key)}"]`)?.scrollIntoView({ block: "center", behavior: "smooth" });
@@ -1545,9 +1590,25 @@ watch(driverStoreTab, (tab) => {
</div>
<!-- Driver List -->
<div class="relative">
<Search class="absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input data-driver-store-agent-search v-model="agentDriverSearch" class="h-8 pl-8 text-xs" :placeholder="t('driverStore.searchDrivers')" />
<div class="flex flex-wrap items-center gap-2">
<div class="relative min-w-0 flex-1">
<Search class="absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input data-driver-store-agent-search v-model="agentDriverSearch" class="h-8 pl-8 text-xs" :placeholder="t('driverStore.searchDrivers')" />
</div>
<div class="driver-store-status-filter flex shrink-0 items-center rounded-md border bg-muted/20 p-0.5" role="group" :aria-label="t('driverStore.statusFilterLabel')">
<button
v-for="option in driverStatusFilterOptions"
:key="option.value"
type="button"
class="h-7 rounded px-2 text-xs transition-colors"
:class="driverStatusFilter === option.value ? 'bg-background font-medium text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
:aria-pressed="driverStatusFilter === option.value"
:data-driver-status-filter="option.value"
@click="driverStatusFilter = option.value"
>
{{ option.label }}
</button>
</div>
</div>
<!-- Global update section — always above category navigation -->
<div v-if="globalUpdatableDrivers.length > 0" class="rounded-lg border divide-y">
@@ -1557,7 +1618,7 @@ watch(driverStoreTab, (tab) => {
<p class="text-xs text-muted-foreground">{{ t("driverStore.updatesAvailableDescription") }}</p>
</div>
<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 || agentPackageBusy" @click="upgradeAll">
<Button v-if="canBatchUpgradeAll" size="sm" class="h-7 rounded-md text-xs shrink-0 ml-3" :disabled="installing !== null || preparingUpgradeAll || upgradingAll || agentPackageBusy" @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") }}
@@ -1567,103 +1628,16 @@ watch(driverStoreTab, (tab) => {
</Button>
</div>
</div>
<div
<DriverStoreAgentRow
v-for="driver in globalUpdatableDrivers"
:key="driver.db_type"
:data-driver-store-focus="`driver:${driver.db_type}`"
class="driver-store-agent-row flex items-center gap-3 px-4 py-2 transition hover:bg-muted/30"
:class="{ 'driver-store-focus-highlight': highlightedFocusKey === `driver:${driver.db_type}` }"
>
<span class="flex h-8 w-8 items-center justify-center rounded-md bg-muted/60 shrink-0">
<DatabaseIcon :db-type="driver.db_type" class="h-4 w-4" />
</span>
<div class="driver-store-agent-name min-w-0 flex-1">
<div class="text-sm font-medium">{{ driver.label }}</div>
</div>
<div class="driver-store-agent-meta flex shrink-0 items-center gap-1.5">
<span v-if="driverRequiresJavaRuntime(driver) && driver.jre" class="rounded-full px-2 py-0.5 text-[11px]" :class="driver.jre !== '21' ? 'bg-blue-500/10 text-blue-600' : 'bg-muted text-muted-foreground'">JRE {{ driver.jre }}</span>
<span v-if="driver.installed" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">v{{ driver.installed_version }}</span>
<span v-if="driver.installed && driver.update_available" class="rounded-full bg-amber-500/15 px-2 py-0.5 text-[11px] text-amber-600">→ v{{ driver.version }}</span>
<span v-if="!driver.installed && driver.version" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">v{{ driver.version }}</span>
<span v-if="formatSize(driver.size)" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">{{ formatSize(driver.size) }}</span>
</div>
<div class="driver-store-agent-actions flex shrink-0 items-center gap-2">
<Button
v-if="!driver.installed && isDriverQueued(driver.db_type)"
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="preparingUpgradeAll || upgradingAll || agentPackageBusy"
@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-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 || agentPackageBusy" @click="installDriver(driver.db_type)">
<Download class="h-3 w-3 mr-1" />
{{ t("driverStore.install") }}
</Button>
<Button
v-if="!driver.installed && !isManagedJdbcBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
size="sm"
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="preparingUpgradeAll || upgradingAll || installing !== null || agentPackageBusy"
@click="importDriverFile(driver)"
>
<Loader2 v-if="importingDriver === driver.db_type" class="h-3.5 w-3.5 animate-spin" />
<FileUp v-else class="h-3.5 w-3.5" />
</Button>
<Check v-if="driver.installed && !(driver.update_available && isDriverProgressActive(driver.db_type))" class="h-4 w-4 text-green-600" />
<Button
v-if="driver.installed && driver.update_available && isDriverQueued(driver.db_type)"
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="preparingUpgradeAll || upgradingAll || agentPackageBusy"
@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-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 || agentPackageBusy" @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 || preparingUpgradeAll || upgradingAll || agentPackageBusy || isDriverQueued(driver.db_type)"
@click="uninstallDriver(driver.db_type)"
>
{{ t("driverStore.uninstall") }}
</Button>
</div>
</div>
v-bind="agentRowProps(driver)"
@install="installDriver(driver.db_type)"
@uninstall="uninstallDriver(driver.db_type)"
@import-file="importDriverFile(driver)"
@cancel-install="cancelDriverInstall(driver.db_type)"
@remove-queue="removeQueuedDriverInstall(driver.db_type)"
/>
</div>
<!-- Category nav + driver list container -->
<div class="min-h-0 flex flex-1 flex-col gap-3 overflow-hidden sm:flex-row sm:gap-0">
@@ -1697,6 +1671,21 @@ watch(driverStoreTab, (tab) => {
<div v-if="drivers.length === 0" class="py-12 text-center text-sm text-muted-foreground">
{{ t("common.loading") }}
</div>
<!-- Empty: nothing installed yet -->
<div v-else-if="showInstalledEmptyState" class="flex flex-col items-center gap-3 py-12 text-center">
<p class="text-sm font-medium">{{ t("driverStore.noInstalledDrivers") }}</p>
<p class="text-xs text-muted-foreground">{{ t("driverStore.noInstalledDriversHint") }}</p>
<Button type="button" variant="outline" size="sm" class="h-7 rounded-md text-xs" @click="driverStatusFilter = 'all'">
{{ t("driverStore.showAllDrivers") }}
</Button>
</div>
<div v-else-if="showAvailableEmptyState" class="flex flex-col items-center gap-3 py-12 text-center">
<p class="text-sm font-medium">{{ t("driverStore.noAvailableDrivers") }}</p>
<p class="text-xs text-muted-foreground">{{ t("driverStore.noAvailableDriversHint") }}</p>
<Button type="button" variant="outline" size="sm" class="h-7 rounded-md text-xs" @click="driverStatusFilter = 'all'">
{{ t("driverStore.showAllDrivers") }}
</Button>
</div>
<!-- Empty: no search results (suppressed when updatable drivers match the search) -->
<div v-else-if="isDriverSearchActive && searchedDrivers.length === 0 && !hasMatchingUpdatableDrivers" class="py-12 text-center text-sm text-muted-foreground">
{{ t("driverStore.noMatchingDrivers") }}
@@ -1711,208 +1700,32 @@ watch(driverStoreTab, (tab) => {
<div v-for="group in searchedDriversByCategory" :key="group.key" class="space-y-2">
<h3 class="px-4 text-sm font-medium text-muted-foreground">{{ group.title }}</h3>
<div class="rounded-lg border divide-y">
<div
<DriverStoreAgentRow
v-for="driver in group.drivers"
:key="driver.db_type"
:data-driver-store-focus="`driver:${driver.db_type}`"
class="driver-store-agent-row flex items-center gap-3 px-4 py-2 transition hover:bg-muted/30"
:class="{ 'driver-store-focus-highlight': highlightedFocusKey === `driver:${driver.db_type}` }"
>
<span class="flex h-8 w-8 items-center justify-center rounded-md bg-muted/60 shrink-0">
<DatabaseIcon :db-type="driver.db_type" class="h-4 w-4" />
</span>
<div class="driver-store-agent-name min-w-0 flex-1">
<div class="text-sm font-medium">{{ driver.label }}</div>
</div>
<span class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">{{ driverCategoryLabel(driver.db_type) }}</span>
<div class="driver-store-agent-meta flex shrink-0 items-center gap-1.5">
<span v-if="driverRequiresJavaRuntime(driver) && driver.jre" class="rounded-full px-2 py-0.5 text-[11px]" :class="driver.jre !== '21' ? 'bg-blue-500/10 text-blue-600' : 'bg-muted text-muted-foreground'">JRE {{ driver.jre }}</span>
<span v-if="driver.installed" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">v{{ driver.installed_version }}</span>
<span v-if="driver.installed && driver.update_available" class="rounded-full bg-amber-500/15 px-2 py-0.5 text-[11px] text-amber-600">→ v{{ driver.version }}</span>
<span v-if="!driver.installed && driver.version" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">v{{ driver.version }}</span>
<span v-if="formatSize(driver.size)" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">{{ formatSize(driver.size) }}</span>
</div>
<div class="driver-store-agent-actions flex shrink-0 items-center gap-2">
<Button
v-if="!driver.installed && isDriverQueued(driver.db_type)"
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="preparingUpgradeAll || upgradingAll || agentPackageBusy"
@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-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 || agentPackageBusy" @click="installDriver(driver.db_type)">
<Download class="h-3 w-3 mr-1" />
{{ t("driverStore.install") }}
</Button>
<Button
v-if="!driver.installed && !isManagedJdbcBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
size="sm"
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="preparingUpgradeAll || upgradingAll || installing !== null || agentPackageBusy"
@click="importDriverFile(driver)"
>
<Loader2 v-if="importingDriver === driver.db_type" class="h-3.5 w-3.5 animate-spin" />
<FileUp v-else class="h-3.5 w-3.5" />
</Button>
<Check v-if="driver.installed && !(driver.update_available && isDriverProgressActive(driver.db_type))" class="h-4 w-4 text-green-600" />
<Button
v-if="driver.installed && driver.update_available && isDriverQueued(driver.db_type)"
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="preparingUpgradeAll || upgradingAll || agentPackageBusy"
@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-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 || agentPackageBusy" @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 || preparingUpgradeAll || upgradingAll || agentPackageBusy || isDriverQueued(driver.db_type)"
@click="uninstallDriver(driver.db_type)"
>
{{ t("driverStore.uninstall") }}
</Button>
</div>
</div>
v-bind="agentRowProps(driver, { showCategoryBadge: true })"
@install="installDriver(driver.db_type)"
@uninstall="uninstallDriver(driver.db_type)"
@import-file="importDriverFile(driver)"
@cancel-install="cancelDriverInstall(driver.db_type)"
@remove-queue="removeQueuedDriverInstall(driver.db_type)"
/>
</div>
</div>
</div>
<!-- Normal view: category-filtered stable drivers (updatable drivers are in the global banner above) -->
<div v-else class="driver-store-agent-list rounded-lg border divide-y">
<!-- Stable driver rows -->
<div
<div v-else-if="categoryStableDrivers.length > 0" class="driver-store-agent-list rounded-lg border divide-y">
<DriverStoreAgentRow
v-for="driver in categoryStableDrivers"
:key="driver.db_type"
:data-driver-store-focus="`driver:${driver.db_type}`"
class="driver-store-agent-row flex items-center gap-3 px-4 py-2 transition hover:bg-muted/30"
:class="{ 'driver-store-focus-highlight': highlightedFocusKey === `driver:${driver.db_type}` }"
>
<span class="flex h-8 w-8 items-center justify-center rounded-md bg-muted/60 shrink-0">
<DatabaseIcon :db-type="driver.db_type" class="h-4 w-4" />
</span>
<div class="driver-store-agent-name min-w-0 flex-1">
<div class="text-sm font-medium">{{ driver.label }}</div>
</div>
<div class="driver-store-agent-meta flex shrink-0 items-center gap-1.5">
<span v-if="driverRequiresJavaRuntime(driver) && driver.jre" class="rounded-full px-2 py-0.5 text-[11px]" :class="driver.jre !== '21' ? 'bg-blue-500/10 text-blue-600' : 'bg-muted text-muted-foreground'">JRE {{ driver.jre }}</span>
<span v-if="driver.installed" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">v{{ driver.installed_version }}</span>
<span v-if="driver.installed && driver.update_available" class="rounded-full bg-amber-500/15 px-2 py-0.5 text-[11px] text-amber-600">→ v{{ driver.version }}</span>
<span v-if="!driver.installed && driver.version" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">v{{ driver.version }}</span>
<span v-if="formatSize(driver.size)" class="rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground">{{ formatSize(driver.size) }}</span>
</div>
<div class="driver-store-agent-actions flex shrink-0 items-center gap-2">
<Button
v-if="!driver.installed && isDriverQueued(driver.db_type)"
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="preparingUpgradeAll || upgradingAll || agentPackageBusy"
@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-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 || agentPackageBusy" @click="installDriver(driver.db_type)">
<Download class="h-3 w-3 mr-1" />
{{ t("driverStore.install") }}
</Button>
<Button
v-if="!driver.installed && !isManagedJdbcBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
size="sm"
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="preparingUpgradeAll || upgradingAll || installing !== null || agentPackageBusy"
@click="importDriverFile(driver)"
>
<Loader2 v-if="importingDriver === driver.db_type" class="h-3.5 w-3.5 animate-spin" />
<FileUp v-else class="h-3.5 w-3.5" />
</Button>
<Check v-if="driver.installed && !(driver.update_available && isDriverProgressActive(driver.db_type))" class="h-4 w-4 text-green-600" />
<Button
v-if="driver.installed && driver.update_available && isDriverQueued(driver.db_type)"
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="preparingUpgradeAll || upgradingAll || agentPackageBusy"
@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-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 || agentPackageBusy" @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 || preparingUpgradeAll || upgradingAll || agentPackageBusy || isDriverQueued(driver.db_type)"
@click="uninstallDriver(driver.db_type)"
>
{{ t("driverStore.uninstall") }}
</Button>
</div>
</div>
v-bind="agentRowProps(driver)"
@install="installDriver(driver.db_type)"
@uninstall="uninstallDriver(driver.db_type)"
@import-file="importDriverFile(driver)"
@cancel-install="cancelDriverInstall(driver.db_type)"
@remove-queue="removeQueuedDriverInstall(driver.db_type)"
/>
</div>
</div>
</div>
@@ -2318,6 +2131,22 @@ watch(driverStoreTab, (tab) => {
align-items: center !important;
min-width: 0;
width: 100%;
position: relative;
}
.driver-store-agent-row--installed::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 2px;
border-radius: 1px 0 0 1px;
background: rgb(22 163 74);
}
html.dark .driver-store-agent-row--installed::before {
background: rgb(74 222 128);
}
.driver-store-agent-list,
@@ -2372,6 +2201,14 @@ html.dbx-legacy-webview .driver-store-storage-tab > :not([hidden]) ~ :not([hidde
margin-top: 0 !important;
}
html.dbx-legacy-webview .driver-store-agent-row--installed::before {
background: rgb(22 163 74) !important;
}
html.dbx-legacy-webview.dark .driver-store-agent-row--installed::before {
background: rgb(74 222 128) !important;
}
html.dbx-legacy-webview .driver-store-local-import-button {
width: 2rem !important;
height: 2rem !important;
+10
View File
@@ -7861,6 +7861,16 @@ export default withEnglishFallback({
jdbcSourceMaven: "Maven",
jdbcSourceManual: "Əl ilə",
noMatchingDrivers: "Uyğun drayver yoxdur.",
statusFilterLabel: "Quraşdırma statusu",
statusFilterAll: "Hamısı {count}",
statusFilterInstalled: "Quraşdırılıb {count}",
statusFilterAvailable: "Quraşdırılmayıb {count}",
installedBadge: "Quraşdırılıb",
noInstalledDrivers: "Hələ quraşdırılmış daxili drayver yoxdur.",
noInstalledDriversHint: "Lazım olan drayverləri tapıb quraşdırmaq üçün Hamısı-na keçin.",
noAvailableDrivers: "Bütün daxili drayverlər artıq quraşdırılıb.",
noAvailableDriversHint: "Bu kompüterdəki drayverləri idarə etmək üçün Hamısı və ya Quraşdırılıb-a keçin.",
showAllDrivers: "Bütün drayverləri göstər",
javaRuntimeSaved: "Java icra mühiti parametrləri yadda saxlanıldı",
javaRuntimeSaveFailed: "Java icra mühiti parametrlərini yadda saxlamaq mümkün olmadı: {error}",
chooseJavaExecutable: "Java icra faylını seç",
+10
View File
@@ -8052,6 +8052,16 @@ export default {
jdbcSourceMaven: "Maven",
jdbcSourceManual: "Manual",
noMatchingDrivers: "No matching drivers.",
statusFilterLabel: "Install status",
statusFilterAll: "All {count}",
statusFilterInstalled: "Installed {count}",
statusFilterAvailable: "Available {count}",
installedBadge: "Installed",
noInstalledDrivers: "No built-in drivers installed yet.",
noInstalledDriversHint: "Switch to All to browse and install the drivers you need.",
noAvailableDrivers: "Every built-in driver is already installed.",
noAvailableDriversHint: "Switch to All or Installed to manage the drivers on this machine.",
showAllDrivers: "Show all drivers",
javaRuntimeSaved: "Java runtime settings saved",
javaRuntimeSaveFailed: "Failed to save Java runtime settings: {error}",
chooseJavaExecutable: "Choose Java executable",
+10
View File
@@ -7603,6 +7603,16 @@ export default withEnglishFallback({
jdbcSourceMaven: "Maven",
jdbcSourceManual: "Manual",
noMatchingDrivers: "Sin coincidencias.",
statusFilterLabel: "Estado de instalación",
statusFilterAll: "Todos {count}",
statusFilterInstalled: "Instalados {count}",
statusFilterAvailable: "Disponibles {count}",
installedBadge: "Instalado",
noInstalledDrivers: "Aún no hay controladores integrados instalados.",
noInstalledDriversHint: "Cambia a Todos para buscar e instalar los controladores que necesitas.",
noAvailableDrivers: "Todos los controladores integrados ya están instalados.",
noAvailableDriversHint: "Cambia a Todos o Instalados para gestionar los controladores de este equipo.",
showAllDrivers: "Mostrar todos los controladores",
javaRuntimeSaved: "Ajustes de Java guardados",
javaRuntimeSaveFailed: "Error al guardar los ajustes de Java: {error}",
chooseJavaExecutable: "Seleccionar ejecutable Java",
+10
View File
@@ -7603,6 +7603,16 @@ export default withEnglishFallback({
jdbcSourceMaven: "Maven",
jdbcSourceManual: "Manuale",
noMatchingDrivers: "Nessun driver corrispondente.",
statusFilterLabel: "Stato di installazione",
statusFilterAll: "Tutti {count}",
statusFilterInstalled: "Installati {count}",
statusFilterAvailable: "Disponibili {count}",
installedBadge: "Installato",
noInstalledDrivers: "Nessun driver integrato installato.",
noInstalledDriversHint: "Passa a Tutti per sfogliare e installare i driver necessari.",
noAvailableDrivers: "Tutti i driver integrati sono già installati.",
noAvailableDriversHint: "Passa a Tutti o Installati per gestire i driver su questo computer.",
showAllDrivers: "Mostra tutti i driver",
javaRuntimeSaved: "Impostazioni Java Runtime salvate",
javaRuntimeSaveFailed: "Impossibile salvare le impostazioni del runtime Java: {error}",
chooseJavaExecutable: "Scegli l'eseguibile Java",
+10
View File
@@ -7655,6 +7655,16 @@ export default withEnglishFallback({
jdbcSourceMaven: "Maven",
jdbcSourceManual: "手動",
noMatchingDrivers: "一致するドライバーがありません。",
statusFilterLabel: "インストール状態",
statusFilterAll: "すべて {count}",
statusFilterInstalled: "インストール済み {count}",
statusFilterAvailable: "未インストール {count}",
installedBadge: "インストール済み",
noInstalledDrivers: "組み込みドライバーはまだインストールされていません。",
noInstalledDriversHint: "「すべて」に切り替えて、必要なドライバーを探してインストールできます。",
noAvailableDrivers: "組み込みドライバーはすべてインストール済みです。",
noAvailableDriversHint: "「すべて」または「インストール済み」に切り替えて、このマシンのドライバーを管理できます。",
showAllDrivers: "すべてのドライバーを表示",
javaRuntimeSaved: "Javaランタイム設定を保存しました",
javaRuntimeSaveFailed: "Javaランタイム設定の保存に失敗しました: {error}",
chooseJavaExecutable: "Java実行ファイルを選択",
+10
View File
@@ -7335,6 +7335,16 @@ export default withEnglishFallback({
jdbcSourceMaven: "Maven",
jdbcSourceManual: "수동",
noMatchingDrivers: "일치하는 드라이버가 없습니다.",
statusFilterLabel: "설치 상태",
statusFilterAll: "전체 {count}",
statusFilterInstalled: "설치됨 {count}",
statusFilterAvailable: "미설치 {count}",
installedBadge: "설치됨",
noInstalledDrivers: "설치된 기본 드라이버가 없습니다.",
noInstalledDriversHint: "전체로 전환해 필요한 드라이버를 찾아 설치하세요.",
noAvailableDrivers: "모든 기본 드라이버가 이미 설치되어 있습니다.",
noAvailableDriversHint: "전체 또는 설치됨으로 전환해 이 기기의 드라이버를 관리하세요.",
showAllDrivers: "모든 드라이버 보기",
javaRuntimeSaved: "Java 런타임 설정을 저장했습니다",
javaRuntimeSaveFailed: "Java 런타임 설정 저장 실패: {error}",
chooseJavaExecutable: "Java 실행 파일 선택",
+10
View File
@@ -7605,6 +7605,16 @@ export default withEnglishFallback({
jdbcSourceMaven: "Maven",
jdbcSourceManual: "Manual",
noMatchingDrivers: "Nenhum driver correspondente.",
statusFilterLabel: "Status da instalação",
statusFilterAll: "Todos {count}",
statusFilterInstalled: "Instalados {count}",
statusFilterAvailable: "Disponíveis {count}",
installedBadge: "Instalado",
noInstalledDrivers: "Nenhum driver integrado instalado ainda.",
noInstalledDriversHint: "Mude para Todos para procurar e instalar os drivers necessários.",
noAvailableDrivers: "Todos os drivers integrados já estão instalados.",
noAvailableDriversHint: "Mude para Todos ou Instalados para gerenciar os drivers neste computador.",
showAllDrivers: "Mostrar todos os drivers",
javaRuntimeSaved: "Configurações de runtime do Java salvas",
javaRuntimeSaveFailed: "Falha ao salvar configurações de runtime do Java: {error}",
chooseJavaExecutable: "Escolher executável do Java",
+10
View File
@@ -7713,6 +7713,16 @@ export default withEnglishFallback({
jdbcSourceMaven: "Maven",
jdbcSourceManual: "Elle",
noMatchingDrivers: "Eşleşen sürücü yok.",
statusFilterLabel: "Kurulum durumu",
statusFilterAll: "Tümü {count}",
statusFilterInstalled: "Yüklü {count}",
statusFilterAvailable: "Yüklü değil {count}",
installedBadge: "Yüklü",
noInstalledDrivers: "Henüz yüklü yerleşik sürücü yok.",
noInstalledDriversHint: "Gerekli sürücüleri bulup yüklemek için Tümü’ne geçin.",
noAvailableDrivers: "Tüm yerleşik sürücüler zaten yüklü.",
noAvailableDriversHint: "Bu makinedeki sürücüleri yönetmek için Tümü veya Yüklü’ye geçin.",
showAllDrivers: "Tüm sürücüleri göster",
javaRuntimeSaved: "Java çalışma zamanı ayarları kaydedildi",
javaRuntimeSaveFailed: "Java çalışma zamanı ayarları kaydedilemedi: {error}",
chooseJavaExecutable: "Java çalıştırılabilirini seçin",
+10
View File
@@ -8024,6 +8024,16 @@ export default withEnglishFallback({
jdbcSourceMaven: "Maven",
jdbcSourceManual: "手动",
noMatchingDrivers: "没有匹配的驱动。",
statusFilterLabel: "安装状态",
statusFilterAll: "全部 {count}",
statusFilterInstalled: "已安装 {count}",
statusFilterAvailable: "未安装 {count}",
installedBadge: "已安装",
noInstalledDrivers: "还没有安装内置驱动。",
noInstalledDriversHint: "切换到「全部」浏览并安装需要的驱动。",
noAvailableDrivers: "所有内置驱动都已安装。",
noAvailableDriversHint: "切换到「全部」或「已安装」管理本机驱动。",
showAllDrivers: "查看全部驱动",
javaRuntimeSaved: "Java 运行时设置已保存",
javaRuntimeSaveFailed: "Java 运行时设置失败: {error}",
chooseJavaExecutable: "选择 Java 可执行文件",
+10
View File
@@ -7057,6 +7057,16 @@ export default withEnglishFallback({
jdbcSourceMaven: "Maven",
jdbcSourceManual: "手動",
noMatchingDrivers: "沒有相符的驅動程式。",
statusFilterLabel: "安裝狀態",
statusFilterAll: "全部 {count}",
statusFilterInstalled: "已安裝 {count}",
statusFilterAvailable: "未安裝 {count}",
installedBadge: "已安裝",
noInstalledDrivers: "尚未安裝內建驅動程式。",
noInstalledDriversHint: "切換到「全部」瀏覽並安裝需要的驅動程式。",
noAvailableDrivers: "所有內建驅動程式都已安裝。",
noAvailableDriversHint: "切換到「全部」或「已安裝」管理本機驅動程式。",
showAllDrivers: "查看全部驅動程式",
driverInstallCancelled: "{label} 驅動安裝已取消",
upgradeAllCancelled: "批次升級已取消({count} 個驅動中止)",
cancelInstall: "取消安裝",
@@ -5,6 +5,7 @@ const tauri = readFileSync(new URL("../tauri.ts", import.meta.url), "utf8");
const http = readFileSync(new URL("../http.ts", import.meta.url), "utf8");
const api = readFileSync(new URL("../api.ts", import.meta.url), "utf8");
const driverStore = readFileSync(new URL("../../../components/config/DriverStoreDialog.vue", import.meta.url), "utf8");
const driverStoreAgentRow = readFileSync(new URL("../../../components/config/DriverStoreAgentRow.vue", import.meta.url), "utf8");
const tauriRegistry = readFileSync(new URL("../../../../../../src-tauri/src/lib.rs", import.meta.url), "utf8");
const webRegistry = readFileSync(new URL("../../../../../../crates/dbx-web/src/main.rs", import.meta.url), "utf8");
@@ -91,10 +92,19 @@ describe("offline Agent export transport contract", () => {
expect(busyContract).toContain(dependency);
}
for (const handler of ["upgradeAll", "installDriver(driver.db_type)", "importDriverFile(driver)", "uninstallDriver(driver.db_type)", "reinstallJre(jre.key)", "uninstallJre(jre.key)"]) {
for (const handler of ["upgradeAll", "reinstallJre(jre.key)", "uninstallJre(jre.key)"]) {
for (const button of buttonTagsForClick(handler)) expect(button).toContain("agentPackageBusy");
}
expect(driverStore).toContain("packageBusy: agentPackageBusy.value");
expect(driverStore).toContain('@install="installDriver(driver.db_type)"');
expect(driverStore).toContain('@uninstall="uninstallDriver(driver.db_type)"');
expect(driverStore).toContain('@import-file="importDriverFile(driver)"');
expect(driverStoreAgentRow).toContain("packageBusy");
expect(driverStoreAgentRow).toContain(':disabled="queueOrUpgradeBusy"');
expect(driverStoreAgentRow).toContain(':disabled="preparingUpgradeAll || upgradingAll || installing || packageBusy"');
expect(driverStoreAgentRow).toContain(':disabled="uninstallBusy"');
expect(sourceSection(driverStore, "async function importOfflineZip", "\n\nasync function importDriverFile")).toContain("if (agentExportImportBlocked.value) return;");
});
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { agentDriverInstallKey, driverStoreFocusForInstallError, hasInstalledAgentVersion, showAgentDriverInstallHint } from "@/lib/connection/agentDriverInstallHint";
import { agentDriverInstallKey, driverStoreFocusElementKey, driverStoreFocusForInstallError, driverStoreFocusRowIsRenderable, hasInstalledAgentVersion, shouldApplyDriverStoreFocus, showAgentDriverInstallHint } from "@/lib/connection/agentDriverInstallHint";
describe("DuckDB driver installation", () => {
it("requires the downloadable DuckDB driver before connecting", () => {
@@ -28,6 +28,49 @@ describe("HiveServer2-compatible driver installation", () => {
});
});
describe("shouldApplyDriverStoreFocus", () => {
it("applies when the driver first appears after a list load", () => {
expect(shouldApplyDriverStoreFocus(null, "driver:mysql", false)).toBe(true);
});
it("does not re-apply on inventory refresh of the same focus", () => {
expect(shouldApplyDriverStoreFocus("driver:mysql", "driver:mysql", false)).toBe(false);
});
it("re-applies when the parent sends a new focus object for the same driver", () => {
expect(shouldApplyDriverStoreFocus("driver:mysql", "driver:mysql", true)).toBe(true);
});
it("applies when the focused driver changes", () => {
expect(shouldApplyDriverStoreFocus("driver:mysql", "driver:postgres", true)).toBe(true);
});
});
describe("driverStoreFocusElementKey", () => {
it("keys driver and jre focus targets", () => {
expect(driverStoreFocusElementKey({ target: "driver", driver: "mysql" })).toBe("driver:mysql");
expect(driverStoreFocusElementKey({ target: "jre" })).toBe("jre");
});
});
describe("driverStoreFocusRowIsRenderable", () => {
it("does not treat managed JDBC as renderable while the agent list is still loading", () => {
expect(driverStoreFocusRowIsRenderable({ target: "driver", driver: "phoenix" }, 0, [{ db_type: "phoenix" }])).toBe(false);
});
it("allows focus once the agent list has loaded and the row exists", () => {
expect(driverStoreFocusRowIsRenderable({ target: "driver", driver: "phoenix" }, 1, [{ db_type: "phoenix" }])).toBe(true);
});
it("waits when the focused driver is not in the builtin rows yet", () => {
expect(driverStoreFocusRowIsRenderable({ target: "driver", driver: "mysql" }, 3, [{ db_type: "phoenix" }])).toBe(false);
});
it("treats JRE focus as renderable during agent list loading", () => {
expect(driverStoreFocusRowIsRenderable({ target: "jre" }, 0, [])).toBe(true);
});
});
describe("driverStoreFocusForInstallError", () => {
it("focuses the SQLite SSH worker for remote SQLite driver errors", () => {
expect(driverStoreFocusForInstallError("sqlite-worker driver is not installed. Please install it from the Driver Manager.", "sqlite", undefined)).toEqual({
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { selectStableDrivers, selectUpdatableDrivers, hasAnyUpdatableDriverMatching } from "@/lib/connection/driverListFilter";
import { selectStableDrivers, selectUpdatableDrivers, hasAnyUpdatableDriverMatching, selectDriversByInstallStatus, countInstalledDrivers, countAvailableDrivers, partitionDriversByInstallStatus, upgradeAllDriverTypes, upgradeAllMatchesFullUpdateSet } from "@/lib/connection/driverListFilter";
import type { AgentDriverInfo } from "@/lib/backend/api";
function driver(overrides: Partial<AgentDriverInfo> = {}): AgentDriverInfo {
@@ -17,6 +17,123 @@ function driver(overrides: Partial<AgentDriverInfo> = {}): AgentDriverInfo {
};
}
describe("selectDriversByInstallStatus", () => {
const mysql = driver({ db_type: "mysql", installed: true });
const postgres = driver({ db_type: "postgres", installed: false });
const sqlite = driver({ db_type: "sqlite", installed: true, update_available: true });
it("returns a copy of all drivers for 'all'", () => {
const drivers = [mysql, postgres, sqlite];
const result = selectDriversByInstallStatus(drivers, "all");
expect(result.map((d) => d.db_type)).toEqual(["mysql", "postgres", "sqlite"]);
expect(result).not.toBe(drivers);
});
it("returns only installed drivers, including those with updates", () => {
expect(selectDriversByInstallStatus([mysql, postgres, sqlite], "installed").map((d) => d.db_type)).toEqual(["mysql", "sqlite"]);
});
it("returns only drivers that are not installed", () => {
expect(selectDriversByInstallStatus([mysql, postgres, sqlite], "available").map((d) => d.db_type)).toEqual(["postgres"]);
});
it("returns empty arrays when nothing matches", () => {
expect(selectDriversByInstallStatus([postgres], "installed")).toEqual([]);
expect(selectDriversByInstallStatus([mysql], "available")).toEqual([]);
expect(selectDriversByInstallStatus([], "all")).toEqual([]);
});
});
describe("install-status counts", () => {
it("counts installed and available drivers without double-counting updates", () => {
const drivers = [driver({ db_type: "mysql", installed: true }), driver({ db_type: "postgres", installed: false }), driver({ db_type: "sqlite", installed: true, update_available: true })];
expect(countInstalledDrivers(drivers)).toBe(2);
expect(countAvailableDrivers(drivers)).toBe(1);
});
it("returns zeros for an empty list", () => {
expect(countInstalledDrivers([])).toBe(0);
expect(countAvailableDrivers([])).toBe(0);
});
});
describe("partitionDriversByInstallStatus", () => {
const mysql = driver({ db_type: "mysql", installed: true, update_available: false });
const postgres = driver({ db_type: "postgres", installed: false, update_available: false });
const sqlite = driver({ db_type: "sqlite", installed: true, update_available: true });
// Stale registry / missing jar: not installed, but update_available is true.
const staleOracle = driver({ db_type: "oracle", installed: false, update_available: true });
it("keeps uninstalled update_available drivers in the available list and hides the banner", () => {
const { updatable, stable } = partitionDriversByInstallStatus([mysql, postgres, sqlite, staleOracle], "available");
expect(updatable).toEqual([]);
expect(stable.map((d) => d.db_type)).toEqual(["postgres", "oracle"]);
});
it("does not put uninstalled update_available drivers in the installed banner", () => {
const { updatable, stable } = partitionDriversByInstallStatus([mysql, postgres, sqlite, staleOracle], "installed");
expect(updatable.map((d) => d.db_type)).toEqual(["sqlite"]);
expect(stable.map((d) => d.db_type)).toEqual(["mysql"]);
});
it("keeps the all-status partition aligned with updatable vs stable", () => {
const drivers = [mysql, postgres, sqlite, staleOracle];
const { updatable, stable } = partitionDriversByInstallStatus(drivers, "all");
expect(updatable.map((d) => d.db_type)).toEqual(["sqlite", "oracle"]);
expect(stable.map((d) => d.db_type)).toEqual(["mysql", "postgres"]);
expect(updatable.length + stable.length).toBe(drivers.length);
});
it("returns empty sides when the status filter matches nothing", () => {
expect(partitionDriversByInstallStatus([postgres], "installed")).toEqual({ updatable: [], stable: [] });
expect(partitionDriversByInstallStatus([mysql], "available")).toEqual({ updatable: [], stable: [] });
});
it("does not copy the naive available composition that drops stale updatables", () => {
expect(selectStableDrivers(selectDriversByInstallStatus([staleOracle], "available"))).toEqual([]);
expect(partitionDriversByInstallStatus([staleOracle], "available").stable.map((d) => d.db_type)).toEqual(["oracle"]);
});
it("installed banner+list cover every installed driver exactly once", () => {
const drivers = [mysql, postgres, sqlite, staleOracle];
const { updatable, stable } = partitionDriversByInstallStatus(drivers, "installed");
expect(updatable.length + stable.length).toBe(countInstalledDrivers(drivers));
});
it("available list covers every uninstalled driver exactly once", () => {
const drivers = [mysql, postgres, sqlite, staleOracle];
const { updatable, stable } = partitionDriversByInstallStatus(drivers, "available");
expect(updatable).toEqual([]);
expect(stable.length).toBe(countAvailableDrivers(drivers));
});
it("Upgrade All keys match the visible banner, not hidden stale uninstalled rows", () => {
const drivers = [mysql, postgres, sqlite, staleOracle];
expect(upgradeAllDriverTypes(drivers, "installed")).toEqual(["sqlite"]);
expect(upgradeAllMatchesFullUpdateSet(drivers, "installed")).toBe(false);
expect(upgradeAllDriverTypes(drivers, "all")).toEqual(["sqlite", "oracle"]);
expect(upgradeAllMatchesFullUpdateSet(drivers, "all")).toBe(true);
expect(upgradeAllDriverTypes(drivers, "available")).toEqual([]);
expect(upgradeAllMatchesFullUpdateSet(drivers, "available")).toBe(false);
});
it("allows the batch Upgrade All button when every updatable is visible", () => {
expect(upgradeAllMatchesFullUpdateSet([sqlite], "installed")).toBe(true);
expect(upgradeAllDriverTypes([sqlite], "installed")).toEqual(["sqlite"]);
});
it("does not treat equal-sized but different updatable sets as a safe batch", () => {
const installedOther = driver({ db_type: "mysql", installed: true, update_available: false });
const visibleInstalled = driver({ db_type: "sqlite", installed: true, update_available: true });
const hiddenStale = driver({ db_type: "oracle", installed: false, update_available: true });
expect(upgradeAllDriverTypes([installedOther, visibleInstalled, hiddenStale], "installed")).toEqual(["sqlite"]);
expect(upgradeAllMatchesFullUpdateSet([installedOther, visibleInstalled, hiddenStale], "installed")).toBe(false);
});
});
describe("selectUpdatableDrivers", () => {
it("returns only drivers with update_available === true", () => {
const drivers: AgentDriverInfo[] = [driver({ db_type: "mysql", update_available: true }), driver({ db_type: "postgres", update_available: false }), driver({ db_type: "sqlite", update_available: true })];
@@ -190,4 +307,28 @@ describe("hasAnyUpdatableDriverMatching", () => {
}),
).toBe(true);
});
it("returns false when the install-status filter is 'available' (update banner is hidden)", () => {
expect(
hasAnyUpdatableDriverMatching([neo4j], {
searchQuery: "",
selectedCategory: "all",
driverMatchesSearch: labelSearch,
driverCategory: categoryOf,
installStatus: "available",
}),
).toBe(false);
});
it("still matches updatable drivers when the install-status filter is 'installed'", () => {
expect(
hasAnyUpdatableDriverMatching([neo4j], {
searchQuery: "",
selectedCategory: "all",
driverMatchesSearch: labelSearch,
driverCategory: categoryOf,
installStatus: "installed",
}),
).toBe(true);
});
});
@@ -80,6 +80,31 @@ export type DriverStoreTab = "agent" | "jdbc" | "storage" | "runtime";
export type DriverStoreFocus = { target: "driver"; driver?: string } | { target: "jre" } | { target: "tab"; tab: DriverStoreTab };
export function driverStoreFocusElementKey(focus: DriverStoreFocus): string {
return focus.target === "driver" ? `driver:${focus.driver ?? ""}` : "jre";
}
/**
* Apply search/category/status reset + scroll only once per focus key, or when
* the parent hands us a new focus object (user re-triggered the same hint).
* Inventory refreshes must not clobber the status filter.
*/
export function shouldApplyDriverStoreFocus(lastAppliedKey: string | null, nextKey: string, focusChanged: boolean): boolean {
if (focusChanged) return true;
return lastAppliedKey !== nextKey;
}
/**
* Agent list `v-if="drivers.length === 0"` is a loading placeholder that hides
* every row, including managed JDBC rows already present in `builtinDriverRows`.
* Committing focus before that would mark the key applied and skip the later scroll.
*/
export function driverStoreFocusRowIsRenderable(focus: DriverStoreFocus, agentDriverCount: number, builtinRows: readonly { db_type: string }[]): boolean {
if (focus.target !== "driver") return true;
if (agentDriverCount === 0) return false;
return builtinRows.some((row) => row.db_type === focus.driver);
}
/** Maps a backend connect error to the Driver Store item that can fix it. */
export function driverStoreFocusForInstallError(message: string, dbType?: DatabaseType, driverProfile?: string): DriverStoreFocus | null {
if (message.includes("JRE") && message.includes("not installed")) return { target: "jre" };
@@ -1,5 +1,66 @@
import type { AgentDriverInfo } from "@/lib/backend/api";
/** Orthogonal to category: inventory vs catalog. Default is `"all"`. */
export type DriverInstallStatusFilter = "all" | "installed" | "available";
export function selectDriversByInstallStatus(drivers: readonly AgentDriverInfo[], status: DriverInstallStatusFilter): AgentDriverInfo[] {
if (status === "installed") return drivers.filter((d) => d.installed);
if (status === "available") return drivers.filter((d) => !d.installed);
return [...drivers];
}
export function countInstalledDrivers(drivers: readonly AgentDriverInfo[]): number {
return drivers.reduce((count, driver) => count + (driver.installed ? 1 : 0), 0);
}
export function countAvailableDrivers(drivers: readonly AgentDriverInfo[]): number {
return drivers.length - countInstalledDrivers(drivers);
}
export interface PartitionedDriverList {
updatable: AgentDriverInfo[];
stable: AgentDriverInfo[];
}
/**
* Banner vs list partition for the current install-status filter.
*
* The update banner is hidden on `"available"`. Uninstalled rows with
* `update_available` (stale local registry / missing jar) must stay in the
* list, not disappear from both surfaces. On `"installed"` the banner only
* includes installed updatables.
*/
export function partitionDriversByInstallStatus(drivers: readonly AgentDriverInfo[], status: DriverInstallStatusFilter): PartitionedDriverList {
const statusFiltered = selectDriversByInstallStatus(drivers, status);
if (status === "available") {
return { updatable: [], stable: statusFiltered };
}
const updatable = selectUpdatableDrivers(statusFiltered);
const updatableKeys = new Set(updatable.map((driver) => driver.db_type));
return {
updatable,
stable: statusFiltered.filter((driver) => !updatableKeys.has(driver.db_type)),
};
}
/** Driver keys the visible update banner would upgrade. */
export function upgradeAllDriverTypes(drivers: readonly AgentDriverInfo[], status: DriverInstallStatusFilter): string[] {
return partitionDriversByInstallStatus(drivers, status).updatable.map((driver) => driver.db_type);
}
/**
* True when a batch `upgradeAllAgents` (every `update_available` row) would
* match the visible banner. False on Installed when a stale uninstalled
* updatable exists — per-row Update stays, the global batch button does not.
*/
export function upgradeAllMatchesFullUpdateSet(drivers: readonly AgentDriverInfo[], status: DriverInstallStatusFilter): boolean {
const visible = upgradeAllDriverTypes(drivers, status);
const all = selectUpdatableDrivers(drivers).map((driver) => driver.db_type);
if (visible.length !== all.length) return false;
const visibleKeys = new Set(visible);
return all.every((key) => visibleKeys.has(key));
}
/**
* Drivers that have an update available — always rendered above category
* navigation so the user can see and act on them regardless of the active
@@ -27,6 +88,11 @@ export interface UpdatableDriverMatchOptions {
driverMatchesSearch: (driver: AgentDriverInfo, query: string) => boolean;
/** Returns the category key for `driver`. */
driverCategory: (driver: AgentDriverInfo) => string;
/**
* Current install-status filter. Updatable drivers are already installed, so
* `"available"` hides the update banner and must not suppress the empty state.
*/
installStatus?: DriverInstallStatusFilter;
}
/**
@@ -38,6 +104,7 @@ export interface UpdatableDriverMatchOptions {
*/
export function hasAnyUpdatableDriverMatching(updatableDrivers: readonly AgentDriverInfo[], opts: UpdatableDriverMatchOptions): boolean {
if (updatableDrivers.length === 0) return false;
if (opts.installStatus === "available") return false;
if (opts.searchQuery) {
return updatableDrivers.some((d) => opts.driverMatchesSearch(d, opts.searchQuery));
}
@@ -18,6 +18,7 @@ const activeConnectionFilterSource = readFileSync(new URL("../../components/side
const scheduledDatabaseBackupSource = readFileSync(new URL("../../components/backup/ScheduledDatabaseBackupSettings.vue", import.meta.url), "utf8");
const databaseBackupConfigFieldsSource = readFileSync(new URL("../../components/backup/DatabaseBackupConfigFields.vue", import.meta.url), "utf8");
const driverStoreDialogSource = readFileSync(new URL("../../components/config/DriverStoreDialog.vue", import.meta.url), "utf8");
const driverStoreAgentRowSource = readFileSync(new URL("../../components/config/DriverStoreAgentRow.vue", import.meta.url), "utf8");
const tunnelProfileManagerSource = readFileSync(new URL("../../components/connection/TunnelProfileManager.vue", import.meta.url), "utf8");
const changelogPanelSource = readFileSync(new URL("../../components/settings/ChangelogPanel.vue", import.meta.url), "utf8");
const editorSettingsDialogSource = readFileSync(new URL("../../components/editor/EditorSettingsDialog.vue", import.meta.url), "utf8");
@@ -343,8 +344,9 @@ describe("legacy WebView CSS fallbacks", () => {
const fallbackEnd = driverStoreDialogSource.indexOf("@media (max-width: 900px)", fallbackStart);
const fallback = driverStoreDialogSource.slice(fallbackStart, fallbackEnd);
expect(driverStoreDialogSource.match(/driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground/g)?.length).toBe(3);
expect(driverStoreDialogSource.match(/variant="ghost"\n\s+class="driver-store-local-import-button/g)?.length).toBe(3);
expect(driverStoreAgentRowSource.match(/driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground/g)?.length).toBe(1);
expect(driverStoreAgentRowSource.match(/variant="ghost"\n\s+class="driver-store-local-import-button/g)?.length).toBe(1);
expect(driverStoreDialogSource).toContain("<DriverStoreAgentRow");
expect(fallback).toContain(".driver-store-local-import-button");
expect(fallback).toContain("width: 2rem !important;");
expect(fallback).toContain("height: 2rem !important;");
+42
View File
@@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { test } from "vitest";
function source(relativePath: string): string {
return readFileSync(path.resolve(relativePath), "utf8");
}
test("Driver Manager exposes an install-status filter next to search", () => {
const driverStore = source("apps/desktop/src/components/config/DriverStoreDialog.vue");
assert.match(driverStore, /data-driver-status-filter/);
assert.match(driverStore, /driverStatusFilter = ref<DriverInstallStatusFilter>\("all"\)/);
assert.match(driverStore, /partitionDriversByInstallStatus\(builtinDriverRows\.value, driverStatusFilter\.value\)/);
assert.match(driverStore, /upgradeAllDriverTypes\(builtinDriverRows\.value, driverStatusFilter\.value\)/);
assert.match(driverStore, /if \(!upgradeAllMatchesFullUpdateSet\(builtinDriverRows\.value, driverStatusFilter\.value\)\) return/);
assert.match(driverStore, /upgradeAllMatchesFullUpdateSet\(builtinDriverRows\.value, driverStatusFilter\.value\)/);
assert.match(driverStore, /driverStatusFilter\.value = "all"/);
assert.match(driverStore, /shouldApplyDriverStoreFocus\(lastAppliedFocusKey\.value, key, focusChanged\)/);
assert.match(driverStore, /driverStoreFocusRowIsRenderable\(focus, drivers\.value\.length, builtinDriverRows\.value\)/);
const applyIdx = driverStore.indexOf("if (!shouldApplyDriverStoreFocus");
const tabIdx = driverStore.indexOf('driverStoreTab.value = "agent"');
assert.ok(applyIdx >= 0, "focus apply guard");
assert.ok(tabIdx > applyIdx, "Agent tab switch must sit behind the one-shot focus guard");
assert.match(driverStore, /showInstalledEmptyState/);
assert.match(driverStore, /showAvailableEmptyState/);
assert.match(driverStore, /driverStore\.noAvailableDrivers/);
assert.doesNotMatch(driverStore, /selectStableDrivers\(categoryFilteredDrivers/);
assert.doesNotMatch(driverStore, /selectUpdatableDrivers\(builtinDriverRows/);
assert.equal([...driverStore.matchAll(/<DriverStoreAgentRow/g)].length, 3);
});
test("driver rows show a text Installed badge beside the name", () => {
const row = source("apps/desktop/src/components/config/DriverStoreAgentRow.vue");
assert.match(row, /driverStore\.installedBadge/);
assert.match(row, /driver-store-installed-badge/);
assert.match(row, /driver-store-agent-row--installed/);
assert.match(row, /data-driver-store-focus/);
assert.match(row, /driver-store-local-import-button/);
});