mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
feat(driver-store): filter built-in drivers by install status
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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ç",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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実行ファイルを選択",
|
||||
|
||||
@@ -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 실행 파일 선택",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 可执行文件",
|
||||
|
||||
@@ -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;");
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
Reference in New Issue
Block a user