feat(agent): export managed drivers for offline use
This commit is contained in:
1 parent
6eb8b43701
commit
82e5756325
23 files changed
+3419
-40
No files matched your search
@@ -0,0 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { FileArchive, Loader2, ShieldCheck } from "@lucide/vue";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import type { AgentOfflineExportCandidate, AgentOfflineExportPreview, AgentOfflineExportUnavailableReason } from "@/lib/backend/api";
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
preview: AgentOfflineExportPreview | null;
|
||||
loading?: boolean;
|
||||
exporting?: boolean;
|
||||
error?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:open": [value: boolean];
|
||||
confirm: [driverKeys: string[]];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const selectedDriverKeys = ref<string[]>([]);
|
||||
|
||||
const dialogOpen = computed({
|
||||
get: () => props.open,
|
||||
set: (value) => {
|
||||
if (props.exporting && !value) return;
|
||||
emit("update:open", value);
|
||||
},
|
||||
});
|
||||
|
||||
const candidates = computed(() => props.preview?.candidates ?? []);
|
||||
const eligibleCandidates = computed(() => candidates.value.filter((candidate) => candidate.eligible));
|
||||
const selectedCandidates = computed(() => eligibleCandidates.value.filter((candidate) => selectedDriverKeys.value.includes(candidate.dbType)));
|
||||
const selectedJres = computed(() => new Set(selectedCandidates.value.map((candidate) => candidate.requiredJre).filter((jre): jre is string => Boolean(jre))));
|
||||
const selectedCount = computed(() => selectedDriverKeys.value.length);
|
||||
const canConfirm = computed(() => selectedCount.value > 0 && !props.loading && !props.exporting && !props.error);
|
||||
|
||||
watch(
|
||||
() => [props.open, props.preview] as const,
|
||||
([open]) => {
|
||||
if (open && props.preview) {
|
||||
selectedDriverKeys.value = eligibleCandidates.value.map((candidate) => candidate.dbType);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function isSelected(dbType: string) {
|
||||
return selectedDriverKeys.value.includes(dbType);
|
||||
}
|
||||
|
||||
function toggle(dbType: string, checked: boolean) {
|
||||
if (checked) {
|
||||
if (!selectedDriverKeys.value.includes(dbType)) {
|
||||
selectedDriverKeys.value = [...selectedDriverKeys.value, dbType];
|
||||
}
|
||||
return;
|
||||
}
|
||||
selectedDriverKeys.value = selectedDriverKeys.value.filter((selected) => selected !== dbType);
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
selectedDriverKeys.value = eligibleCandidates.value.map((candidate) => candidate.dbType);
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedDriverKeys.value = [];
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
if (!canConfirm.value) return;
|
||||
emit("confirm", [...selectedDriverKeys.value]);
|
||||
}
|
||||
|
||||
function formatSize(bytes: number) {
|
||||
if (!bytes) return "";
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function artifactLabel(candidate: AgentOfflineExportCandidate) {
|
||||
if (candidate.artifactKind === "jar") return t("driverStore.offlineExportArtifactJar");
|
||||
if (candidate.artifactKind === "native") return t("driverStore.offlineExportArtifactNative");
|
||||
return t("driverStore.offlineExportArtifactUnknown");
|
||||
}
|
||||
|
||||
function unavailableReason(reason: AgentOfflineExportUnavailableReason | null) {
|
||||
return reason ? t(`driverStore.offlineExportUnavailable_${reason}`) : "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="dialogOpen">
|
||||
<DialogContent class="sm:max-w-[640px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<FileArchive class="h-5 w-5" />
|
||||
{{ t("driverStore.offlineExportTitle") }}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="grid gap-3 py-2" :aria-busy="loading || exporting">
|
||||
<p class="text-sm text-muted-foreground">{{ t("driverStore.offlineExportDescription") }}</p>
|
||||
|
||||
<div v-if="preview" class="flex flex-wrap items-center justify-between gap-2 rounded-md border bg-muted/30 px-3 py-2 text-xs">
|
||||
<span>
|
||||
{{ t("driverStore.offlineExportPlatform") }}
|
||||
<Badge variant="outline" class="ml-1 font-mono">{{ preview.platform }}</Badge>
|
||||
</span>
|
||||
<span class="text-muted-foreground">
|
||||
{{ t("driverStore.offlineExportSelectedCount", { selected: selectedCount, total: eligibleCandidates.length }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" role="status" aria-live="polite" class="flex h-64 items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("driverStore.offlineExportPreparing") }}
|
||||
</div>
|
||||
<div v-else-if="error" role="alert" class="flex h-48 items-center justify-center rounded-md border border-destructive/30 bg-destructive/5 px-6 text-center text-sm text-destructive">
|
||||
{{ error }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button type="button" variant="outline" size="sm" :disabled="exporting || eligibleCandidates.length === 0" @click="selectAll">
|
||||
{{ t("driverStore.offlineExportSelectAll") }}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" :disabled="exporting || selectedCount === 0" @click="clearSelection">
|
||||
{{ t("driverStore.offlineExportClear") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea class="h-72 rounded-md border">
|
||||
<div v-if="candidates.length === 0" class="px-4 py-12 text-center text-sm text-muted-foreground">
|
||||
{{ t("driverStore.offlineExportNoDrivers") }}
|
||||
</div>
|
||||
<label v-for="candidate in candidates" :key="candidate.dbType" class="flex items-start gap-3 border-b px-3 py-2.5 last:border-b-0" :class="candidate.eligible ? 'cursor-pointer hover:bg-muted/50' : 'cursor-not-allowed bg-muted/20 opacity-70'">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="mt-1 h-3.5 w-3.5 shrink-0 accent-primary"
|
||||
:checked="isSelected(candidate.dbType)"
|
||||
:disabled="!candidate.eligible || exporting"
|
||||
:aria-label="candidate.label"
|
||||
:aria-describedby="!candidate.eligible ? `offline-export-reason-${candidate.dbType}` : undefined"
|
||||
@change="toggle(candidate.dbType, ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<DatabaseIcon :db-type="candidate.dbType" class="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="flex min-w-0 flex-wrap items-center gap-1.5">
|
||||
<span class="truncate text-sm font-medium">{{ candidate.label }}</span>
|
||||
<Badge variant="outline" class="h-5 rounded-full px-2 text-[10px]">v{{ candidate.version }}</Badge>
|
||||
<Badge v-if="candidate.artifactKind" variant="secondary" class="h-5 rounded-full px-2 text-[10px]">{{ artifactLabel(candidate) }}</Badge>
|
||||
<Badge v-if="candidate.requiredJre" variant="secondary" class="h-5 rounded-full px-2 text-[10px]">JRE {{ candidate.requiredJre }}</Badge>
|
||||
<span v-if="candidate.size" class="text-[11px] text-muted-foreground">{{ formatSize(candidate.size) }}</span>
|
||||
</span>
|
||||
<span v-if="!candidate.eligible" :id="`offline-export-reason-${candidate.dbType}`" class="mt-0.5 block text-xs text-muted-foreground">
|
||||
{{ unavailableReason(candidate.unavailableReason) }}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</ScrollArea>
|
||||
|
||||
<div class="rounded-md border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
|
||||
{{ t("driverStore.offlineExportJreSummary", { count: selectedJres.size }) }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-2 rounded-md border border-green-500/20 bg-green-500/5 px-3 py-2 text-xs text-muted-foreground">
|
||||
<ShieldCheck class="mt-0.5 h-4 w-4 shrink-0 text-green-600" />
|
||||
<span>{{ t("driverStore.offlineExportSecurityHint") }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" :disabled="exporting" @click="dialogOpen = false">
|
||||
{{ t("common.cancel") }}
|
||||
</Button>
|
||||
<Button type="button" :disabled="!canConfirm" @click="confirm">
|
||||
<Loader2 v-if="exporting" class="mr-1.5 h-4 w-4 animate-spin" />
|
||||
{{ exporting ? t("driverStore.offlineExporting") : t("driverStore.offlineExportStart", { count: selectedCount }) }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</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, FileUp, X } from "@lucide/vue";
|
||||
import { Activity, ExternalLink, Cpu, FolderOpen, FolderSync, MemoryStick, Search, Square, Trash2, Download, RotateCcw, Loader2, RefreshCw, Check, Clock3, 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,6 +10,7 @@ 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 AgentOfflineExportDialog from "@/components/config/AgentOfflineExportDialog.vue";
|
||||
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
@@ -17,7 +18,7 @@ import { uuid } from "@/lib/common/utils";
|
||||
import { countAvailableDriverUpdates } from "@/lib/connection/agentDriverUpdateBadge";
|
||||
import type { JdbcDriverInfo, JdbcLocalBundleInfo, JdbcMavenBundleInfo, JdbcPluginStatus } from "@/types/database";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { AgentDriverInfo, DriverRuntimeInfo, DriverRuntimeSummary, DriverStoreUsage, JavaRuntimeConfig } from "@/lib/backend/api";
|
||||
import type { AgentDriverInfo, AgentOfflineExportPreview, DriverRuntimeInfo, DriverRuntimeSummary, DriverStoreUsage, JavaRuntimeConfig } from "@/lib/backend/api";
|
||||
import { formatRuntimeBytes, formatRuntimeCpu, formatRuntimeUptime, runtimeHealthClass, runtimeStatusClass, runtimeStatusDotClass } from "@/lib/connection/driverRuntimePresentation";
|
||||
import {
|
||||
addDriverInstallQueue,
|
||||
@@ -36,6 +37,7 @@ import { installRegisteredManagedJdbcDriver, isManagedJdbcDriver, managedJdbcDri
|
||||
import type { DriverStoreFocus } 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";
|
||||
|
||||
@@ -205,6 +207,13 @@ const customJavaPath = ref("");
|
||||
const savingJavaRuntime = ref(false);
|
||||
const driverStoreUsage = ref<DriverStoreUsage | null>(null);
|
||||
const clearingDownloadCache = ref(false);
|
||||
const offlineExportDialogOpen = ref(false);
|
||||
const offlineExportPreview = ref<AgentOfflineExportPreview | null>(null);
|
||||
const offlineExportLoading = ref(false);
|
||||
const offlineExporting = ref(false);
|
||||
const offlineExportError = ref("");
|
||||
const uninstallingDriver = ref<string | null>(null);
|
||||
const uninstallingJre = ref<string | null>(null);
|
||||
const runtimeSummary = ref<DriverRuntimeSummary | null>(null);
|
||||
const runtimeLoading = ref(false);
|
||||
const runtimeError = ref("");
|
||||
@@ -468,6 +477,7 @@ async function chooseCustomJavaPath() {
|
||||
}
|
||||
|
||||
async function installDriver(dbType: string) {
|
||||
if (agentPackageBusy.value) return;
|
||||
if (installing.value !== null || preparingUpgradeAll.value || upgradingAll.value) {
|
||||
queueDriverInstall(dbType);
|
||||
return;
|
||||
@@ -519,7 +529,7 @@ async function runDriverInstall(dbType: string) {
|
||||
}
|
||||
|
||||
async function runQueuedDriverInstalls() {
|
||||
if (installing.value !== null || preparingUpgradeAll.value || upgradingAll.value) return;
|
||||
if (agentPackageBusy.value || installing.value !== null || preparingUpgradeAll.value || upgradingAll.value) return;
|
||||
|
||||
const result = takeNextDriverInstallQueue(queuedDriverInstalls.value, canInstallOrUpdateDriver);
|
||||
queuedDriverInstalls.value = result.queue;
|
||||
@@ -530,7 +540,7 @@ async function runQueuedDriverInstalls() {
|
||||
}
|
||||
|
||||
async function upgradeAll() {
|
||||
if (preparingUpgradeAll.value || upgradingAll.value) return;
|
||||
if (agentPackageBusy.value || preparingUpgradeAll.value || upgradingAll.value) return;
|
||||
preparingUpgradeAll.value = true;
|
||||
try {
|
||||
const updatableDbTypes = drivers.value.filter((driver) => driver.update_available).map((driver) => driver.db_type);
|
||||
@@ -595,7 +605,9 @@ async function cancelUpgradeAll() {
|
||||
}
|
||||
|
||||
async function uninstallDriver(dbType: string) {
|
||||
if (agentPackageBusy.value) return;
|
||||
const label = driverLabel(dbType);
|
||||
uninstallingDriver.value = dbType;
|
||||
try {
|
||||
const managedResult = await uninstallRegisteredManagedJdbcDriver(dbType, jdbcMavenBundles.value, api);
|
||||
if (managedResult) {
|
||||
@@ -615,12 +627,65 @@ async function uninstallDriver(dbType: string) {
|
||||
toast(t("driverStore.driverUninstallSuccess", { label }));
|
||||
} catch (e: any) {
|
||||
toast(t("driverStore.driverUninstallFailed", { label, error: backendError(e) }));
|
||||
} finally {
|
||||
uninstallingDriver.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
const importingZip = ref(false);
|
||||
const importingDriver = ref<string | null>(null);
|
||||
const agentImportBusy = computed(() => importingZip.value || importingDriver.value !== null);
|
||||
const agentPackageBusy = computed(() => agentImportBusy.value || offlineExportLoading.value || offlineExporting.value || uninstallingDriver.value !== null || uninstallingJre.value !== null);
|
||||
const agentExportImportBlocked = computed(() => agentPackageBusy.value || installing.value !== null || preparingUpgradeAll.value || upgradingAll.value || reinstallingJre.value !== null || queuedDriverInstalls.value.length > 0);
|
||||
|
||||
async function openOfflineExportDialog() {
|
||||
if (isWeb || agentExportImportBlocked.value) return;
|
||||
offlineExportDialogOpen.value = true;
|
||||
offlineExportLoading.value = true;
|
||||
offlineExportPreview.value = null;
|
||||
offlineExportError.value = "";
|
||||
try {
|
||||
offlineExportPreview.value = await api.previewAgentOfflineExport();
|
||||
} catch (error) {
|
||||
offlineExportError.value = t("driverStore.offlineExportPreviewFailed", { error: backendError(error) });
|
||||
} finally {
|
||||
offlineExportLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function exportOfflinePackage(driverKeys: string[]) {
|
||||
if (isWeb || agentExportImportBlocked.value || driverKeys.length === 0) return;
|
||||
const platform = offlineExportPreview.value?.platform ?? "current-platform";
|
||||
await runAgentOfflineExportAction({
|
||||
driverKeys,
|
||||
setBusy: (busy) => {
|
||||
offlineExporting.value = busy;
|
||||
},
|
||||
chooseDestination: async () => {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
return save({
|
||||
title: t("driverStore.offlineExportChooseDestination"),
|
||||
defaultPath: `dbx-agents-offline-${platform}.zip`,
|
||||
filters: [{ name: "ZIP", extensions: ["zip"] }],
|
||||
});
|
||||
},
|
||||
exportPackage: api.exportAgentsOffline,
|
||||
onSuccess: ({ destination, result }) => {
|
||||
offlineExportDialogOpen.value = false;
|
||||
toast(
|
||||
t("driverStore.offlineExportSuccess", {
|
||||
drivers: result.driverCount,
|
||||
jres: result.jreCount,
|
||||
size: formatBytes(result.bytes),
|
||||
file: destination.split(/[/\\]/).pop() || destination,
|
||||
}),
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast(t("driverStore.offlineExportFailed", { error: backendError(error) }), 8000);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function chooseWebOfflineZip(): Promise<File | null> {
|
||||
return new Promise((resolve) => {
|
||||
@@ -661,7 +726,7 @@ function chooseWebFile(accept: string): Promise<File | null> {
|
||||
}
|
||||
|
||||
async function importOfflineZip() {
|
||||
if (agentImportBusy.value) return;
|
||||
if (agentExportImportBlocked.value) return;
|
||||
let selected: string | File | null = null;
|
||||
if (isWeb) {
|
||||
selected = await chooseWebOfflineZip();
|
||||
@@ -692,7 +757,7 @@ async function importOfflineZip() {
|
||||
}
|
||||
|
||||
async function importDriverFile(driver: AgentDriverInfo) {
|
||||
if (agentImportBusy.value) return;
|
||||
if (agentPackageBusy.value) return;
|
||||
const dbType = driver.db_type;
|
||||
if (isManagedJdbcBuiltinDriver(dbType)) {
|
||||
await importJdbcDrivers();
|
||||
@@ -753,6 +818,7 @@ async function importDriverFile(driver: AgentDriverInfo) {
|
||||
}
|
||||
|
||||
async function reinstallJre(jreKey: string) {
|
||||
if (agentPackageBusy.value) return;
|
||||
reinstallingJre.value = jreKey;
|
||||
activeAgentOperationId.value = uuid();
|
||||
resetAgentInstallProgress();
|
||||
@@ -770,12 +836,16 @@ async function reinstallJre(jreKey: string) {
|
||||
}
|
||||
|
||||
async function uninstallJre(jreKey: string) {
|
||||
if (agentPackageBusy.value) return;
|
||||
uninstallingJre.value = jreKey;
|
||||
try {
|
||||
await api.uninstallJre(jreKey);
|
||||
await refreshAgents();
|
||||
toast(t("driverStore.jreUninstallSuccess", { jre: jreKey }));
|
||||
} catch (e: any) {
|
||||
toast(String(e));
|
||||
} finally {
|
||||
uninstallingJre.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1400,7 +1470,12 @@ watch(driverStoreTab, (tab) => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button v-if="driverStoreTab === 'agent'" variant="ghost" size="sm" class="h-7 rounded-md text-xs gap-1 text-muted-foreground" :disabled="agentImportBusy || installing !== null || upgradingAll || reinstallingJre !== null || queuedDriverInstalls.length > 0" @click="importOfflineZip">
|
||||
<Button v-if="driverStoreTab === 'agent' && !isWeb" data-testid="agent-offline-export-button" variant="ghost" size="sm" class="h-7 rounded-md text-xs gap-1 text-muted-foreground" :disabled="agentExportImportBlocked" @click="openOfflineExportDialog">
|
||||
<Loader2 v-if="offlineExportLoading || offlineExporting" class="h-3.5 w-3.5 animate-spin" />
|
||||
<FileArchive v-else class="h-3.5 w-3.5" />
|
||||
{{ offlineExporting ? t("driverStore.offlineExporting") : t("driverStore.offlineExport") }}
|
||||
</Button>
|
||||
<Button v-if="driverStoreTab === 'agent'" data-testid="agent-offline-import-button" variant="ghost" size="sm" class="h-7 rounded-md text-xs gap-1 text-muted-foreground" :disabled="agentExportImportBlocked" @click="importOfflineZip">
|
||||
<Loader2 v-if="agentImportBusy" class="h-3.5 w-3.5 animate-spin" />
|
||||
<FileUp v-else class="h-3.5 w-3.5" />
|
||||
{{ agentImportBusy ? t("driverStore.importing") : t("driverStore.importOfflinePackage") }}
|
||||
@@ -1453,15 +1528,15 @@ watch(driverStoreTab, (tab) => {
|
||||
<Check v-if="jre.installed" class="h-4 w-4 text-green-600" />
|
||||
<span v-else class="text-xs text-muted-foreground">{{ t("driverStore.notInstalled") }}</span>
|
||||
<DriverInstallProgressCircle v-if="reinstallingJre === jre.key" :percent="getJreReinstallPercent()" :title="getJreReinstallTitle(jre.installed ? t('driverStore.reinstalling') : t('driverStore.installing'))" />
|
||||
<Button v-else-if="!jre.installed" type="button" variant="default" size="sm" class="h-8 rounded-md text-xs" :disabled="reinstallingJre !== null || installing !== null || agentImportBusy" @click="reinstallJre(jre.key)">
|
||||
<Button v-else-if="!jre.installed" type="button" variant="default" size="sm" class="h-8 rounded-md text-xs" :disabled="reinstallingJre !== null || installing !== null || agentPackageBusy" @click="reinstallJre(jre.key)">
|
||||
<Download class="h-3.5 w-3.5 mr-1" />
|
||||
{{ t("driverStore.install") }}
|
||||
</Button>
|
||||
<Button v-else-if="jre.installed" type="button" variant="outline" size="sm" class="h-8 rounded-md text-xs" :disabled="reinstallingJre !== null || installing !== null || agentImportBusy" @click="reinstallJre(jre.key)">
|
||||
<Button v-else-if="jre.installed" type="button" variant="outline" size="sm" class="h-8 rounded-md text-xs" :disabled="reinstallingJre !== null || installing !== null || agentPackageBusy" @click="reinstallJre(jre.key)">
|
||||
<RotateCcw class="h-3.5 w-3.5 mr-1" />
|
||||
{{ t("driverStore.reinstall") }}
|
||||
</Button>
|
||||
<Button v-if="jre.installed" type="button" variant="ghost" size="sm" class="h-8 rounded-md text-xs text-muted-foreground hover:text-destructive" :disabled="reinstallingJre !== null || installing !== null || agentImportBusy" @click="uninstallJre(jre.key)">
|
||||
<Button v-if="jre.installed" type="button" variant="ghost" size="sm" class="h-8 rounded-md text-xs text-muted-foreground hover:text-destructive" :disabled="reinstallingJre !== null || installing !== null || agentPackageBusy" @click="uninstallJre(jre.key)">
|
||||
{{ t("driverStore.uninstall") }}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1482,7 +1557,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 || agentImportBusy" @click="upgradeAll">
|
||||
<Button 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") }}
|
||||
@@ -1518,7 +1593,7 @@ watch(driverStoreTab, (tab) => {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || agentImportBusy"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || agentPackageBusy"
|
||||
@click="removeQueuedDriverInstall(driver.db_type)"
|
||||
>
|
||||
<Clock3 class="h-3 w-3 mr-1" />
|
||||
@@ -1535,7 +1610,7 @@ watch(driverStoreTab, (tab) => {
|
||||
@click="cancelDriverInstall(driver.db_type)"
|
||||
><X class="h-3.5 w-3.5"
|
||||
/></Button>
|
||||
<Button v-else-if="!driver.installed && !isDriverProgressActive(driver.db_type)" size="sm" class="h-7 rounded-md text-xs" :disabled="preparingUpgradeAll || upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
|
||||
<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>
|
||||
@@ -1545,7 +1620,7 @@ watch(driverStoreTab, (tab) => {
|
||||
variant="ghost"
|
||||
class="driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground"
|
||||
:title="importingDriver === driver.db_type ? t('driverStore.importing') : t('driverStore.importLocalJar')"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || installing !== null || agentImportBusy"
|
||||
: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" />
|
||||
@@ -1557,7 +1632,7 @@ watch(driverStoreTab, (tab) => {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || agentImportBusy"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || agentPackageBusy"
|
||||
@click="removeQueuedDriverInstall(driver.db_type)"
|
||||
>
|
||||
<Clock3 class="h-3 w-3 mr-1" />
|
||||
@@ -1574,7 +1649,7 @@ watch(driverStoreTab, (tab) => {
|
||||
@click="cancelDriverInstall(driver.db_type)"
|
||||
><X class="h-3.5 w-3.5"
|
||||
/></Button>
|
||||
<Button v-else-if="driver.installed && driver.update_available && !isDriverProgressActive(driver.db_type)" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="preparingUpgradeAll || upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
|
||||
<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
|
||||
@@ -1582,7 +1657,7 @@ watch(driverStoreTab, (tab) => {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 rounded-md text-xs text-muted-foreground hover:text-destructive"
|
||||
:disabled="installing !== null || preparingUpgradeAll || upgradingAll || agentImportBusy || isDriverQueued(driver.db_type)"
|
||||
:disabled="installing !== null || preparingUpgradeAll || upgradingAll || agentPackageBusy || isDriverQueued(driver.db_type)"
|
||||
@click="uninstallDriver(driver.db_type)"
|
||||
>
|
||||
{{ t("driverStore.uninstall") }}
|
||||
@@ -1663,7 +1738,7 @@ watch(driverStoreTab, (tab) => {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || agentImportBusy"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || agentPackageBusy"
|
||||
@click="removeQueuedDriverInstall(driver.db_type)"
|
||||
>
|
||||
<Clock3 class="h-3 w-3 mr-1" />
|
||||
@@ -1680,7 +1755,7 @@ watch(driverStoreTab, (tab) => {
|
||||
@click="cancelDriverInstall(driver.db_type)"
|
||||
><X class="h-3.5 w-3.5"
|
||||
/></Button>
|
||||
<Button v-else-if="!driver.installed && !isDriverProgressActive(driver.db_type)" size="sm" class="h-7 rounded-md text-xs" :disabled="preparingUpgradeAll || upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
|
||||
<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>
|
||||
@@ -1690,7 +1765,7 @@ watch(driverStoreTab, (tab) => {
|
||||
variant="ghost"
|
||||
class="driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground"
|
||||
:title="importingDriver === driver.db_type ? t('driverStore.importing') : t('driverStore.importLocalJar')"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || installing !== null || agentImportBusy"
|
||||
: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" />
|
||||
@@ -1702,7 +1777,7 @@ watch(driverStoreTab, (tab) => {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || agentImportBusy"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || agentPackageBusy"
|
||||
@click="removeQueuedDriverInstall(driver.db_type)"
|
||||
>
|
||||
<Clock3 class="h-3 w-3 mr-1" />
|
||||
@@ -1719,7 +1794,7 @@ watch(driverStoreTab, (tab) => {
|
||||
@click="cancelDriverInstall(driver.db_type)"
|
||||
><X class="h-3.5 w-3.5"
|
||||
/></Button>
|
||||
<Button v-else-if="driver.installed && driver.update_available && !isDriverProgressActive(driver.db_type)" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="preparingUpgradeAll || upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
|
||||
<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
|
||||
@@ -1727,7 +1802,7 @@ watch(driverStoreTab, (tab) => {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 rounded-md text-xs text-muted-foreground hover:text-destructive"
|
||||
:disabled="installing !== null || preparingUpgradeAll || upgradingAll || agentImportBusy || isDriverQueued(driver.db_type)"
|
||||
:disabled="installing !== null || preparingUpgradeAll || upgradingAll || agentPackageBusy || isDriverQueued(driver.db_type)"
|
||||
@click="uninstallDriver(driver.db_type)"
|
||||
>
|
||||
{{ t("driverStore.uninstall") }}
|
||||
@@ -1767,7 +1842,7 @@ watch(driverStoreTab, (tab) => {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || agentImportBusy"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || agentPackageBusy"
|
||||
@click="removeQueuedDriverInstall(driver.db_type)"
|
||||
>
|
||||
<Clock3 class="h-3 w-3 mr-1" />
|
||||
@@ -1784,7 +1859,7 @@ watch(driverStoreTab, (tab) => {
|
||||
@click="cancelDriverInstall(driver.db_type)"
|
||||
><X class="h-3.5 w-3.5"
|
||||
/></Button>
|
||||
<Button v-else-if="!driver.installed && !isDriverProgressActive(driver.db_type)" size="sm" class="h-7 rounded-md text-xs" :disabled="preparingUpgradeAll || upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
|
||||
<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>
|
||||
@@ -1794,7 +1869,7 @@ watch(driverStoreTab, (tab) => {
|
||||
variant="ghost"
|
||||
class="driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground"
|
||||
:title="importingDriver === driver.db_type ? t('driverStore.importing') : t('driverStore.importLocalJar')"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || installing !== null || agentImportBusy"
|
||||
: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" />
|
||||
@@ -1806,7 +1881,7 @@ watch(driverStoreTab, (tab) => {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="h-7 rounded-md border-green-500/30 bg-green-500/10 text-xs text-green-700 hover:bg-green-500/15"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || agentImportBusy"
|
||||
:disabled="preparingUpgradeAll || upgradingAll || agentPackageBusy"
|
||||
@click="removeQueuedDriverInstall(driver.db_type)"
|
||||
>
|
||||
<Clock3 class="h-3 w-3 mr-1" />
|
||||
@@ -1823,7 +1898,7 @@ watch(driverStoreTab, (tab) => {
|
||||
@click="cancelDriverInstall(driver.db_type)"
|
||||
><X class="h-3.5 w-3.5"
|
||||
/></Button>
|
||||
<Button v-else-if="driver.installed && driver.update_available && !isDriverProgressActive(driver.db_type)" size="sm" variant="outline" class="h-7 rounded-md text-xs" :disabled="preparingUpgradeAll || upgradingAll || agentImportBusy" @click="installDriver(driver.db_type)">
|
||||
<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
|
||||
@@ -1831,7 +1906,7 @@ watch(driverStoreTab, (tab) => {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 rounded-md text-xs text-muted-foreground hover:text-destructive"
|
||||
:disabled="installing !== null || preparingUpgradeAll || upgradingAll || agentImportBusy || isDriverQueued(driver.db_type)"
|
||||
:disabled="installing !== null || preparingUpgradeAll || upgradingAll || agentPackageBusy || isDriverQueued(driver.db_type)"
|
||||
@click="uninstallDriver(driver.db_type)"
|
||||
>
|
||||
{{ t("driverStore.uninstall") }}
|
||||
@@ -2150,6 +2225,7 @@ watch(driverStoreTab, (tab) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AgentOfflineExportDialog v-model:open="offlineExportDialogOpen" :preview="offlineExportPreview" :loading="offlineExportLoading" :exporting="offlineExporting" :error="offlineExportError" @confirm="exportOfflinePackage" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, type App } from "vue";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "@/i18n";
|
||||
import type { AgentOfflineExportPreview } from "@/lib/backend/api";
|
||||
|
||||
vi.mock("@/components/ui/dialog", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
const passthrough = defineComponent({
|
||||
setup(_props, { slots }) {
|
||||
return () => h("div", slots.default?.());
|
||||
},
|
||||
});
|
||||
return { Dialog: passthrough, DialogContent: passthrough, DialogHeader: passthrough, DialogTitle: passthrough, DialogFooter: passthrough };
|
||||
});
|
||||
|
||||
vi.mock("@/components/ui/button", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
return {
|
||||
Button: defineComponent({
|
||||
props: { disabled: Boolean },
|
||||
setup(props, { slots, emit }) {
|
||||
return () => h("button", { disabled: props.disabled, onClick: () => emit("click") }, slots.default?.());
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/ui/badge", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
return {
|
||||
Badge: defineComponent({
|
||||
setup(_props, { slots }) {
|
||||
return () => h("span", slots.default?.());
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/ui/scroll-area", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
return {
|
||||
ScrollArea: defineComponent({
|
||||
setup(_props, { slots }) {
|
||||
return () => h("div", slots.default?.());
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/icons/DatabaseIcon.vue", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
return {
|
||||
default: defineComponent({
|
||||
setup() {
|
||||
return () => h("span");
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
import AgentOfflineExportDialog from "@/components/config/AgentOfflineExportDialog.vue";
|
||||
|
||||
const mountedApps: App[] = [];
|
||||
|
||||
const preview: AgentOfflineExportPreview = {
|
||||
platform: "macos-aarch64",
|
||||
candidates: [
|
||||
{
|
||||
dbType: "duckdb",
|
||||
label: "DuckDB",
|
||||
version: "1.2.3",
|
||||
size: 1024,
|
||||
artifactKind: "jar",
|
||||
requiredJre: "21",
|
||||
eligible: true,
|
||||
unavailableReason: null,
|
||||
},
|
||||
{
|
||||
dbType: "kafka",
|
||||
label: "Apache Kafka",
|
||||
version: "4.0.0",
|
||||
size: 2048,
|
||||
artifactKind: "jar",
|
||||
requiredJre: "21",
|
||||
eligible: false,
|
||||
unavailableReason: "localInstall",
|
||||
},
|
||||
{
|
||||
dbType: "redis",
|
||||
label: "Redis",
|
||||
version: "8.0.0",
|
||||
size: 4096,
|
||||
artifactKind: "native",
|
||||
requiredJre: null,
|
||||
eligible: true,
|
||||
unavailableReason: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
for (const app of mountedApps.splice(0)) app.unmount();
|
||||
while (document.body.firstChild) document.body.removeChild(document.body.firstChild);
|
||||
});
|
||||
|
||||
async function mountDialog(props: Record<string, unknown>, onConfirm = vi.fn()) {
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup() {
|
||||
return () => h(AgentOfflineExportDialog, { ...props, onConfirm });
|
||||
},
|
||||
}),
|
||||
);
|
||||
mountedApps.push(app);
|
||||
app.use(i18n);
|
||||
app.mount(container);
|
||||
await nextTick();
|
||||
return { onConfirm };
|
||||
}
|
||||
|
||||
function buttonContaining(text: string) {
|
||||
return [...document.body.querySelectorAll("button")].find((button) => button.textContent?.includes(text));
|
||||
}
|
||||
|
||||
describe("AgentOfflineExportDialog", () => {
|
||||
it("selects eligible drivers by default and explains excluded candidates", async () => {
|
||||
i18n.global.locale.value = "en";
|
||||
const { onConfirm } = await mountDialog({ open: true, preview });
|
||||
|
||||
expect(document.body.textContent).toContain("2 of 2 selected");
|
||||
expect(document.body.textContent).toContain("1 managed JRE(s) will be included automatically.");
|
||||
expect(document.body.textContent).toContain("Locally imported Agent builds are not included.");
|
||||
|
||||
const inputs = [...document.body.querySelectorAll<HTMLInputElement>("input[type='checkbox']")];
|
||||
expect(inputs).toHaveLength(3);
|
||||
expect(inputs.map((input) => input.checked)).toEqual([true, false, true]);
|
||||
expect(inputs.map((input) => input.disabled)).toEqual([false, true, false]);
|
||||
expect(inputs[1]?.getAttribute("aria-describedby")).toBe("offline-export-reason-kafka");
|
||||
expect(document.getElementById("offline-export-reason-kafka")?.textContent).toContain("Locally imported");
|
||||
|
||||
buttonContaining("Export 2 driver(s)")?.click();
|
||||
expect(onConfirm).toHaveBeenCalledWith(["duckdb", "redis"]);
|
||||
});
|
||||
|
||||
it("disables export after clearing the selection", async () => {
|
||||
i18n.global.locale.value = "en";
|
||||
const { onConfirm } = await mountDialog({ open: true, preview });
|
||||
|
||||
buttonContaining("Clear")?.click();
|
||||
await nextTick();
|
||||
|
||||
expect(document.body.textContent).toContain("0 of 2 selected");
|
||||
const confirm = buttonContaining("Export 0 driver(s)");
|
||||
expect(confirm?.disabled).toBe(true);
|
||||
confirm?.click();
|
||||
expect(onConfirm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("locks controls while an export is running", async () => {
|
||||
i18n.global.locale.value = "en";
|
||||
const { onConfirm } = await mountDialog({ open: true, preview, exporting: true });
|
||||
|
||||
expect(buttonContaining("Select all")?.disabled).toBe(true);
|
||||
expect(buttonContaining("Clear")?.disabled).toBe(true);
|
||||
expect(buttonContaining("Cancel")?.disabled).toBe(true);
|
||||
expect(buttonContaining("Exporting")?.disabled).toBe(true);
|
||||
expect([...document.body.querySelectorAll<HTMLInputElement>("input[type='checkbox']")].every((input) => input.disabled)).toBe(true);
|
||||
buttonContaining("Exporting")?.click();
|
||||
expect(onConfirm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows the empty state when no managed drivers are available", async () => {
|
||||
i18n.global.locale.value = "en";
|
||||
await mountDialog({ open: true, preview: { platform: "linux-x64", candidates: [] } satisfies AgentOfflineExportPreview });
|
||||
|
||||
expect(document.body.textContent).toContain("No eligible DBX-managed Agent drivers are installed.");
|
||||
expect(buttonContaining("Export 0 driver(s)")?.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import en from "../locales/en";
|
||||
import es from "../locales/es";
|
||||
import itLocale from "../locales/it";
|
||||
import ja from "../locales/ja";
|
||||
import ko from "../locales/ko";
|
||||
import ptBR from "../locales/pt-BR";
|
||||
import zhCN from "../locales/zh-CN";
|
||||
import zhTW from "../locales/zh-TW";
|
||||
|
||||
type Messages = Record<string, unknown>;
|
||||
|
||||
const locales: Array<[string, Messages]> = [
|
||||
["es", es],
|
||||
["it", itLocale],
|
||||
["ja", ja],
|
||||
["ko", ko],
|
||||
["pt-BR", ptBR],
|
||||
["zh-CN", zhCN],
|
||||
["zh-TW", zhTW],
|
||||
];
|
||||
|
||||
function offlineExportEntries(locale: Messages): Map<string, string> {
|
||||
const driverStore = locale.driverStore as Record<string, unknown>;
|
||||
return new Map(
|
||||
Object.entries(driverStore)
|
||||
.filter(([key, value]) => key.startsWith("offlineExport") && typeof value === "string")
|
||||
.map(([key, value]) => [key, value as string]),
|
||||
);
|
||||
}
|
||||
|
||||
function placeholders(message: string): string[] {
|
||||
return [...new Set([...message.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]))].sort();
|
||||
}
|
||||
|
||||
describe("offline Agent export i18n parity", () => {
|
||||
const englishEntries = offlineExportEntries(en);
|
||||
|
||||
it("declares the complete English message set", () => {
|
||||
expect(englishEntries.size).toBe(29);
|
||||
});
|
||||
|
||||
it.each(locales)("%s declares matching keys and placeholders", (_name, locale) => {
|
||||
const localeEntries = offlineExportEntries(locale);
|
||||
expect([...localeEntries.keys()].sort()).toEqual([...englishEntries.keys()].sort());
|
||||
|
||||
for (const [key, englishMessage] of englishEntries) {
|
||||
expect(placeholders(localeEntries.get(key) ?? ""), key).toEqual(placeholders(englishMessage));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -6696,6 +6696,35 @@ export default {
|
||||
jdbcDrivers: "JDBC Drivers",
|
||||
importing: "Importing...",
|
||||
importOfflinePackage: "Import offline package",
|
||||
offlineExport: "Export offline package",
|
||||
offlineExportTitle: "Export offline Agent package",
|
||||
offlineExportDescription: "Select the DBX-managed Agent drivers to include. The package is compatible with the current platform only.",
|
||||
offlineExportPlatform: "Platform",
|
||||
offlineExportSelectedCount: "{selected} of {total} selected",
|
||||
offlineExportSelectAll: "Select all",
|
||||
offlineExportClear: "Clear",
|
||||
offlineExportNoDrivers: "No eligible DBX-managed Agent drivers are installed.",
|
||||
offlineExportArtifactJar: "Java JAR",
|
||||
offlineExportArtifactNative: "Native",
|
||||
offlineExportArtifactUnknown: "Unknown",
|
||||
offlineExportPreparing: "Preparing export preview...",
|
||||
offlineExportJreSummary: "{count} managed JRE(s) will be included automatically.",
|
||||
offlineExportSecurityHint: "Only DBX-managed artifacts and required managed JREs are included. Local paths, settings, and external drivers are excluded.",
|
||||
offlineExportStart: "Export {count} driver(s)",
|
||||
offlineExporting: "Exporting...",
|
||||
offlineExportPreviewFailed: "Unable to prepare the export: {error}",
|
||||
offlineExportChooseDestination: "Save offline Agent package",
|
||||
offlineExportSuccess: "Exported {file}: {drivers} driver(s), {jres} JRE(s), {size}.",
|
||||
offlineExportFailed: "Offline package export failed: {error}",
|
||||
offlineExportUnavailable_unmanagedInstall: "This artifact is not tracked as a DBX-managed installation.",
|
||||
offlineExportUnavailable_localInstall: "Locally imported Agent builds are not included.",
|
||||
offlineExportUnavailable_launchConfig: "Custom launch configurations are machine-specific.",
|
||||
offlineExportUnavailable_missingArtifact: "The installed Agent artifact is missing.",
|
||||
offlineExportUnavailable_invalidArtifact: "The installed Agent artifact is invalid or corrupt.",
|
||||
offlineExportUnavailable_unsafeSource: "The artifact is outside the managed store or uses a symbolic link.",
|
||||
offlineExportUnavailable_externalDriverRequired: "This Agent requires a user-provided external driver.",
|
||||
offlineExportUnavailable_missingManagedJre: "The required DBX-managed JRE is not installed.",
|
||||
offlineExportUnavailable_invalidManagedJre: "The required DBX-managed JRE is invalid or unsafe.",
|
||||
refresh: "Refresh",
|
||||
javaRuntime: "Java Runtime",
|
||||
javaRuntimeManaged: "DBX managed JRE",
|
||||
|
||||
@@ -6407,6 +6407,35 @@ export default withEnglishFallback({
|
||||
jdbcDrivers: "Drivers JDBC",
|
||||
importing: "Importando...",
|
||||
importOfflinePackage: "Importar paquete offline",
|
||||
offlineExport: "Exportar paquete offline",
|
||||
offlineExportTitle: "Exportar paquete de Agent offline",
|
||||
offlineExportDescription: "Selecciona los drivers Agent administrados por DBX que se incluirán. El paquete solo es compatible con la plataforma actual.",
|
||||
offlineExportPlatform: "Plataforma",
|
||||
offlineExportSelectedCount: "{selected} de {total} seleccionados",
|
||||
offlineExportSelectAll: "Seleccionar todo",
|
||||
offlineExportClear: "Limpiar",
|
||||
offlineExportNoDrivers: "No hay drivers Agent administrados por DBX que se puedan exportar.",
|
||||
offlineExportArtifactJar: "JAR de Java",
|
||||
offlineExportArtifactNative: "Nativo",
|
||||
offlineExportArtifactUnknown: "Desconocido",
|
||||
offlineExportPreparing: "Preparando la vista previa...",
|
||||
offlineExportJreSummary: "Se incluirán automáticamente {count} JRE administrados.",
|
||||
offlineExportSecurityHint: "Solo se incluyen artefactos administrados por DBX y los JRE necesarios. Se excluyen rutas locales, configuraciones y drivers externos.",
|
||||
offlineExportStart: "Exportar {count} driver(s)",
|
||||
offlineExporting: "Exportando...",
|
||||
offlineExportPreviewFailed: "No se pudo preparar la exportación: {error}",
|
||||
offlineExportChooseDestination: "Guardar paquete de Agent offline",
|
||||
offlineExportSuccess: "Se exportó {file}: {drivers} driver(s), {jres} JRE, {size}.",
|
||||
offlineExportFailed: "Error al exportar el paquete offline: {error}",
|
||||
offlineExportUnavailable_unmanagedInstall: "El artefacto no está registrado como una instalación administrada por DBX.",
|
||||
offlineExportUnavailable_localInstall: "Las compilaciones Agent importadas localmente no se incluyen.",
|
||||
offlineExportUnavailable_launchConfig: "Las configuraciones de inicio personalizadas son específicas de este equipo.",
|
||||
offlineExportUnavailable_missingArtifact: "Falta el artefacto Agent instalado.",
|
||||
offlineExportUnavailable_invalidArtifact: "El artefacto Agent instalado no es válido o está dañado.",
|
||||
offlineExportUnavailable_unsafeSource: "El artefacto está fuera del almacén administrado o usa un enlace simbólico.",
|
||||
offlineExportUnavailable_externalDriverRequired: "Este Agent requiere un driver externo proporcionado por el usuario.",
|
||||
offlineExportUnavailable_missingManagedJre: "El JRE administrado por DBX requerido no está instalado.",
|
||||
offlineExportUnavailable_invalidManagedJre: "El JRE administrado por DBX requerido no es válido o no es seguro.",
|
||||
refresh: "Actualizar",
|
||||
javaRuntime: "Runtime Java",
|
||||
javaRuntimeManaged: "JRE administrado por DBX",
|
||||
|
||||
@@ -6407,6 +6407,35 @@ export default withEnglishFallback({
|
||||
jdbcDrivers: "Driver JDBC",
|
||||
importing: "Importazione in corso...",
|
||||
importOfflinePackage: "Importa pacchetto offline",
|
||||
offlineExport: "Esporta pacchetto offline",
|
||||
offlineExportTitle: "Esporta pacchetto Agent offline",
|
||||
offlineExportDescription: "Seleziona i driver Agent gestiti da DBX da includere. Il pacchetto è compatibile solo con la piattaforma corrente.",
|
||||
offlineExportPlatform: "Piattaforma",
|
||||
offlineExportSelectedCount: "{selected} di {total} selezionati",
|
||||
offlineExportSelectAll: "Seleziona tutto",
|
||||
offlineExportClear: "Deseleziona",
|
||||
offlineExportNoDrivers: "Non sono installati driver Agent gestiti da DBX idonei all'esportazione.",
|
||||
offlineExportArtifactJar: "JAR Java",
|
||||
offlineExportArtifactNative: "Nativo",
|
||||
offlineExportArtifactUnknown: "Sconosciuto",
|
||||
offlineExportPreparing: "Preparazione dell'anteprima...",
|
||||
offlineExportJreSummary: "Verranno inclusi automaticamente {count} JRE gestiti.",
|
||||
offlineExportSecurityHint: "Sono inclusi solo gli artefatti gestiti da DBX e i JRE necessari. Percorsi locali, impostazioni e driver esterni sono esclusi.",
|
||||
offlineExportStart: "Esporta {count} driver",
|
||||
offlineExporting: "Esportazione in corso...",
|
||||
offlineExportPreviewFailed: "Impossibile preparare l'esportazione: {error}",
|
||||
offlineExportChooseDestination: "Salva pacchetto Agent offline",
|
||||
offlineExportSuccess: "Esportato {file}: {drivers} driver, {jres} JRE, {size}.",
|
||||
offlineExportFailed: "Esportazione del pacchetto offline non riuscita: {error}",
|
||||
offlineExportUnavailable_unmanagedInstall: "L'artefatto non è registrato come installazione gestita da DBX.",
|
||||
offlineExportUnavailable_localInstall: "Le build Agent importate localmente non sono incluse.",
|
||||
offlineExportUnavailable_launchConfig: "Le configurazioni di avvio personalizzate sono specifiche del computer.",
|
||||
offlineExportUnavailable_missingArtifact: "L'artefatto Agent installato è mancante.",
|
||||
offlineExportUnavailable_invalidArtifact: "L'artefatto Agent installato non è valido o è danneggiato.",
|
||||
offlineExportUnavailable_unsafeSource: "L'artefatto è fuori dall'archivio gestito o usa un collegamento simbolico.",
|
||||
offlineExportUnavailable_externalDriverRequired: "Questo Agent richiede un driver esterno fornito dall'utente.",
|
||||
offlineExportUnavailable_missingManagedJre: "Il JRE gestito da DBX richiesto non è installato.",
|
||||
offlineExportUnavailable_invalidManagedJre: "Il JRE gestito da DBX richiesto non è valido o sicuro.",
|
||||
refresh: "Aggiorna",
|
||||
javaRuntime: "Java Runtime",
|
||||
javaRuntimeManaged: "JRE gestito da DBX",
|
||||
|
||||
@@ -6460,6 +6460,35 @@ export default withEnglishFallback({
|
||||
jdbcDrivers: "JDBCドライバー",
|
||||
importing: "インポート中...",
|
||||
importOfflinePackage: "オフラインパッケージをインポート",
|
||||
offlineExport: "オフラインパッケージをエクスポート",
|
||||
offlineExportTitle: "Agentオフラインパッケージをエクスポート",
|
||||
offlineExportDescription: "含めるDBX管理のAgentドライバーを選択します。パッケージは現在のプラットフォームでのみ使用できます。",
|
||||
offlineExportPlatform: "対応プラットフォーム",
|
||||
offlineExportSelectedCount: "{total}件中{selected}件を選択",
|
||||
offlineExportSelectAll: "すべて選択",
|
||||
offlineExportClear: "選択を解除",
|
||||
offlineExportNoDrivers: "エクスポート可能なDBX管理のAgentドライバーがインストールされていません。",
|
||||
offlineExportArtifactJar: "Java JAR",
|
||||
offlineExportArtifactNative: "ネイティブ",
|
||||
offlineExportArtifactUnknown: "不明",
|
||||
offlineExportPreparing: "エクスポート内容を準備中...",
|
||||
offlineExportJreSummary: "必要な管理対象JREを{count}件、自動的に含めます。",
|
||||
offlineExportSecurityHint: "DBX管理の成果物と必要なJREのみを含めます。ローカルパス、設定、外部ドライバーは除外されます。",
|
||||
offlineExportStart: "{count}件のドライバーをエクスポート",
|
||||
offlineExporting: "エクスポート中...",
|
||||
offlineExportPreviewFailed: "エクスポートを準備できませんでした: {error}",
|
||||
offlineExportChooseDestination: "Agentオフラインパッケージを保存",
|
||||
offlineExportSuccess: "{file} をエクスポートしました: ドライバー{drivers}件、JRE {jres}件、{size}。",
|
||||
offlineExportFailed: "オフラインパッケージのエクスポートに失敗しました: {error}",
|
||||
offlineExportUnavailable_unmanagedInstall: "この成果物はDBX管理のインストールとして記録されていません。",
|
||||
offlineExportUnavailable_localInstall: "ローカルでインポートしたAgentビルドは含まれません。",
|
||||
offlineExportUnavailable_launchConfig: "カスタム起動設定はこのマシン固有です。",
|
||||
offlineExportUnavailable_missingArtifact: "インストール済みのAgent成果物が見つかりません。",
|
||||
offlineExportUnavailable_invalidArtifact: "インストール済みのAgent成果物が無効または破損しています。",
|
||||
offlineExportUnavailable_unsafeSource: "成果物が管理対象ストア外にあるか、シンボリックリンクを使用しています。",
|
||||
offlineExportUnavailable_externalDriverRequired: "このAgentにはユーザー提供の外部ドライバーが必要です。",
|
||||
offlineExportUnavailable_missingManagedJre: "必要なDBX管理JREがインストールされていません。",
|
||||
offlineExportUnavailable_invalidManagedJre: "必要なDBX管理JREが無効または安全ではありません。",
|
||||
refresh: "更新",
|
||||
javaRuntime: "Javaランタイム",
|
||||
javaRuntimeManaged: "DBX管理JRE",
|
||||
|
||||
@@ -6099,6 +6099,35 @@ export default withEnglishFallback({
|
||||
jdbcDrivers: "JDBC 드라이버",
|
||||
importing: "가져오는 중...",
|
||||
importOfflinePackage: "오프라인 패키지 가져오기",
|
||||
offlineExport: "오프라인 패키지 내보내기",
|
||||
offlineExportTitle: "Agent 오프라인 패키지 내보내기",
|
||||
offlineExportDescription: "포함할 DBX 관리 Agent 드라이버를 선택하세요. 패키지는 현재 플랫폼에서만 사용할 수 있습니다.",
|
||||
offlineExportPlatform: "대상 플랫폼",
|
||||
offlineExportSelectedCount: "{total}개 중 {selected}개 선택",
|
||||
offlineExportSelectAll: "모두 선택",
|
||||
offlineExportClear: "선택 해제",
|
||||
offlineExportNoDrivers: "내보낼 수 있는 DBX 관리 Agent 드라이버가 설치되어 있지 않습니다.",
|
||||
offlineExportArtifactJar: "Java JAR",
|
||||
offlineExportArtifactNative: "네이티브",
|
||||
offlineExportArtifactUnknown: "알 수 없음",
|
||||
offlineExportPreparing: "내보내기 미리보기를 준비하는 중...",
|
||||
offlineExportJreSummary: "필요한 관리 JRE {count}개가 자동으로 포함됩니다.",
|
||||
offlineExportSecurityHint: "DBX 관리 아티팩트와 필요한 JRE만 포함됩니다. 로컬 경로, 설정 및 외부 드라이버는 제외됩니다.",
|
||||
offlineExportStart: "드라이버 {count}개 내보내기",
|
||||
offlineExporting: "내보내는 중...",
|
||||
offlineExportPreviewFailed: "내보내기를 준비할 수 없습니다: {error}",
|
||||
offlineExportChooseDestination: "Agent 오프라인 패키지 저장",
|
||||
offlineExportSuccess: "{file} 내보내기 완료: 드라이버 {drivers}개, JRE {jres}개, {size}.",
|
||||
offlineExportFailed: "오프라인 패키지 내보내기 실패: {error}",
|
||||
offlineExportUnavailable_unmanagedInstall: "이 아티팩트는 DBX 관리 설치로 기록되어 있지 않습니다.",
|
||||
offlineExportUnavailable_localInstall: "로컬에서 가져온 Agent 빌드는 포함되지 않습니다.",
|
||||
offlineExportUnavailable_launchConfig: "사용자 지정 실행 설정은 이 컴퓨터에만 적용됩니다.",
|
||||
offlineExportUnavailable_missingArtifact: "설치된 Agent 아티팩트가 없습니다.",
|
||||
offlineExportUnavailable_invalidArtifact: "설치된 Agent 아티팩트가 유효하지 않거나 손상되었습니다.",
|
||||
offlineExportUnavailable_unsafeSource: "아티팩트가 관리 저장소 밖에 있거나 심볼릭 링크를 사용합니다.",
|
||||
offlineExportUnavailable_externalDriverRequired: "이 Agent에는 사용자가 제공하는 외부 드라이버가 필요합니다.",
|
||||
offlineExportUnavailable_missingManagedJre: "필요한 DBX 관리 JRE가 설치되어 있지 않습니다.",
|
||||
offlineExportUnavailable_invalidManagedJre: "필요한 DBX 관리 JRE가 유효하지 않거나 안전하지 않습니다.",
|
||||
refresh: "새로고침",
|
||||
javaRuntime: "Java 런타임",
|
||||
javaRuntimeManaged: "DBX 관리 JRE",
|
||||
|
||||
@@ -6409,6 +6409,35 @@ export default withEnglishFallback({
|
||||
jdbcDrivers: "Drivers JDBC",
|
||||
importing: "Importando...",
|
||||
importOfflinePackage: "Importar pacote offline",
|
||||
offlineExport: "Exportar pacote offline",
|
||||
offlineExportTitle: "Exportar pacote offline do Agent",
|
||||
offlineExportDescription: "Selecione os drivers Agent gerenciados pelo DBX que serão incluídos. O pacote é compatível apenas com a plataforma atual.",
|
||||
offlineExportPlatform: "Plataforma",
|
||||
offlineExportSelectedCount: "{selected} de {total} selecionados",
|
||||
offlineExportSelectAll: "Selecionar tudo",
|
||||
offlineExportClear: "Limpar",
|
||||
offlineExportNoDrivers: "Nenhum driver Agent gerenciado pelo DBX elegível está instalado.",
|
||||
offlineExportArtifactJar: "JAR Java",
|
||||
offlineExportArtifactNative: "Nativo",
|
||||
offlineExportArtifactUnknown: "Desconhecido",
|
||||
offlineExportPreparing: "Preparando a visualização...",
|
||||
offlineExportJreSummary: "{count} JRE gerenciada(s) serão incluídas automaticamente.",
|
||||
offlineExportSecurityHint: "Somente artefatos gerenciados pelo DBX e as JRE necessárias são incluídos. Caminhos locais, configurações e drivers externos são excluídos.",
|
||||
offlineExportStart: "Exportar {count} driver(s)",
|
||||
offlineExporting: "Exportando...",
|
||||
offlineExportPreviewFailed: "Não foi possível preparar a exportação: {error}",
|
||||
offlineExportChooseDestination: "Salvar pacote offline do Agent",
|
||||
offlineExportSuccess: "{file} exportado: {drivers} driver(s), {jres} JRE, {size}.",
|
||||
offlineExportFailed: "Falha ao exportar o pacote offline: {error}",
|
||||
offlineExportUnavailable_unmanagedInstall: "O artefato não está registrado como uma instalação gerenciada pelo DBX.",
|
||||
offlineExportUnavailable_localInstall: "Builds Agent importadas localmente não são incluídas.",
|
||||
offlineExportUnavailable_launchConfig: "Configurações de inicialização personalizadas são específicas desta máquina.",
|
||||
offlineExportUnavailable_missingArtifact: "O artefato Agent instalado está ausente.",
|
||||
offlineExportUnavailable_invalidArtifact: "O artefato Agent instalado é inválido ou está corrompido.",
|
||||
offlineExportUnavailable_unsafeSource: "O artefato está fora do armazenamento gerenciado ou usa um link simbólico.",
|
||||
offlineExportUnavailable_externalDriverRequired: "Este Agent exige um driver externo fornecido pelo usuário.",
|
||||
offlineExportUnavailable_missingManagedJre: "A JRE gerenciada pelo DBX necessária não está instalada.",
|
||||
offlineExportUnavailable_invalidManagedJre: "A JRE gerenciada pelo DBX necessária é inválida ou insegura.",
|
||||
refresh: "Atualizar",
|
||||
javaRuntime: "Runtime do Java",
|
||||
javaRuntimeManaged: "JRE gerenciada pelo DBX",
|
||||
|
||||
@@ -6681,6 +6681,35 @@ export default withEnglishFallback({
|
||||
jdbcDrivers: "JDBC 驱动",
|
||||
importing: "导入中...",
|
||||
importOfflinePackage: "导入离线包",
|
||||
offlineExport: "导出离线包",
|
||||
offlineExportTitle: "导出 Agent 离线包",
|
||||
offlineExportDescription: "选择要包含的 DBX 托管 Agent 驱动。离线包仅适用于当前平台。",
|
||||
offlineExportPlatform: "适用平台",
|
||||
offlineExportSelectedCount: "已选择 {selected}/{total}",
|
||||
offlineExportSelectAll: "全选",
|
||||
offlineExportClear: "清空",
|
||||
offlineExportNoDrivers: "当前没有可导出的 DBX 托管 Agent 驱动。",
|
||||
offlineExportArtifactJar: "Java JAR",
|
||||
offlineExportArtifactNative: "原生程序",
|
||||
offlineExportArtifactUnknown: "未知",
|
||||
offlineExportPreparing: "正在准备导出预览...",
|
||||
offlineExportJreSummary: "将自动包含 {count} 个所需的托管 JRE。",
|
||||
offlineExportSecurityHint: "仅包含 DBX 托管的驱动产物及所需 JRE;本地路径、配置和外部驱动不会被导出。",
|
||||
offlineExportStart: "导出 {count} 个驱动",
|
||||
offlineExporting: "导出中...",
|
||||
offlineExportPreviewFailed: "无法准备导出:{error}",
|
||||
offlineExportChooseDestination: "保存 Agent 离线包",
|
||||
offlineExportSuccess: "已导出 {file}:{drivers} 个驱动、{jres} 个 JRE,共 {size}。",
|
||||
offlineExportFailed: "离线包导出失败:{error}",
|
||||
offlineExportUnavailable_unmanagedInstall: "该产物未被记录为 DBX 托管安装。",
|
||||
offlineExportUnavailable_localInstall: "本地导入的 Agent 构建不会被打包。",
|
||||
offlineExportUnavailable_launchConfig: "自定义启动配置仅适用于当前机器。",
|
||||
offlineExportUnavailable_missingArtifact: "已安装的 Agent 产物不存在。",
|
||||
offlineExportUnavailable_invalidArtifact: "已安装的 Agent 产物无效或已损坏。",
|
||||
offlineExportUnavailable_unsafeSource: "产物不在托管目录内,或使用了符号链接。",
|
||||
offlineExportUnavailable_externalDriverRequired: "该 Agent 依赖用户自行提供的外部驱动。",
|
||||
offlineExportUnavailable_missingManagedJre: "尚未安装所需的 DBX 托管 JRE。",
|
||||
offlineExportUnavailable_invalidManagedJre: "所需的 DBX 托管 JRE 无效或不安全。",
|
||||
refresh: "刷新",
|
||||
javaRuntime: "Java 运行时",
|
||||
javaRuntimeManaged: "DBX 托管 JRE",
|
||||
|
||||
@@ -5717,6 +5717,35 @@ export default withEnglishFallback({
|
||||
jdbcDrivers: "JDBC 驅動程式",
|
||||
importing: "匯入中……",
|
||||
importOfflinePackage: "匯入離線包",
|
||||
offlineExport: "匯出離線包",
|
||||
offlineExportTitle: "匯出 Agent 離線包",
|
||||
offlineExportDescription: "選擇要包含的 DBX 託管 Agent 驅動程式。離線包僅適用於目前平台。",
|
||||
offlineExportPlatform: "適用平台",
|
||||
offlineExportSelectedCount: "已選擇 {selected}/{total}",
|
||||
offlineExportSelectAll: "全選",
|
||||
offlineExportClear: "清除",
|
||||
offlineExportNoDrivers: "目前沒有可匯出的 DBX 託管 Agent 驅動程式。",
|
||||
offlineExportArtifactJar: "Java JAR",
|
||||
offlineExportArtifactNative: "原生程式",
|
||||
offlineExportArtifactUnknown: "未知",
|
||||
offlineExportPreparing: "正在準備匯出預覽……",
|
||||
offlineExportJreSummary: "將自動包含 {count} 個所需的託管 JRE。",
|
||||
offlineExportSecurityHint: "僅包含 DBX 託管的驅動程式產物及所需 JRE;本機路徑、設定和外部驅動程式不會被匯出。",
|
||||
offlineExportStart: "匯出 {count} 個驅動程式",
|
||||
offlineExporting: "匯出中……",
|
||||
offlineExportPreviewFailed: "無法準備匯出:{error}",
|
||||
offlineExportChooseDestination: "儲存 Agent 離線包",
|
||||
offlineExportSuccess: "已匯出 {file}:{drivers} 個驅動程式、{jres} 個 JRE,共 {size}。",
|
||||
offlineExportFailed: "離線包匯出失敗:{error}",
|
||||
offlineExportUnavailable_unmanagedInstall: "此產物未被記錄為 DBX 託管安裝。",
|
||||
offlineExportUnavailable_localInstall: "本機匯入的 Agent 建置不會被打包。",
|
||||
offlineExportUnavailable_launchConfig: "自訂啟動設定僅適用於目前機器。",
|
||||
offlineExportUnavailable_missingArtifact: "已安裝的 Agent 產物不存在。",
|
||||
offlineExportUnavailable_invalidArtifact: "已安裝的 Agent 產物無效或已損毀。",
|
||||
offlineExportUnavailable_unsafeSource: "產物不在託管目錄內,或使用了符號連結。",
|
||||
offlineExportUnavailable_externalDriverRequired: "此 Agent 依賴使用者自行提供的外部驅動程式。",
|
||||
offlineExportUnavailable_missingManagedJre: "尚未安裝所需的 DBX 託管 JRE。",
|
||||
offlineExportUnavailable_invalidManagedJre: "所需的 DBX 託管 JRE 無效或不安全。",
|
||||
refresh: "重新整理",
|
||||
javaRuntime: "Java 執行環境",
|
||||
javaRuntimeManaged: "DBX 託管 JRE",
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
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 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");
|
||||
|
||||
function functionBody(source: string, operation: string): string {
|
||||
const start = source.indexOf(`export async function ${operation}(`);
|
||||
expect(start, `${operation} transport function`).toBeGreaterThanOrEqual(0);
|
||||
const end = source.indexOf("\nexport async function ", start + 1);
|
||||
return source.slice(start, end === -1 ? source.length : end);
|
||||
}
|
||||
|
||||
function sourceSection(source: string, startMarker: string, endMarker: string): string {
|
||||
const start = source.indexOf(startMarker);
|
||||
expect(start, `source marker ${startMarker}`).toBeGreaterThanOrEqual(0);
|
||||
const end = source.indexOf(endMarker, start + startMarker.length);
|
||||
expect(end, `source marker ${endMarker}`).toBeGreaterThan(start);
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
function buttonTagByTestId(testId: string): string {
|
||||
const marker = `data-testid="${testId}"`;
|
||||
const markerIndex = driverStore.indexOf(marker);
|
||||
expect(markerIndex, marker).toBeGreaterThanOrEqual(0);
|
||||
const start = driverStore.lastIndexOf("<Button", markerIndex);
|
||||
const end = driverStore.indexOf(">", markerIndex);
|
||||
expect(start, `${testId} button start`).toBeGreaterThanOrEqual(0);
|
||||
expect(end, `${testId} button end`).toBeGreaterThan(markerIndex);
|
||||
return driverStore.slice(start, end + 1);
|
||||
}
|
||||
|
||||
function buttonTagsForClick(handler: string): string[] {
|
||||
const marker = `@click="${handler}"`;
|
||||
const tags: string[] = [];
|
||||
let offset = 0;
|
||||
while (true) {
|
||||
const markerIndex = driverStore.indexOf(marker, offset);
|
||||
if (markerIndex === -1) break;
|
||||
const start = driverStore.lastIndexOf("<Button", markerIndex);
|
||||
const end = driverStore.indexOf(">", markerIndex);
|
||||
expect(start, `${handler} button start`).toBeGreaterThanOrEqual(0);
|
||||
expect(end, `${handler} button end`).toBeGreaterThan(markerIndex);
|
||||
tags.push(driverStore.slice(start, end + 1));
|
||||
offset = end + 1;
|
||||
}
|
||||
expect(tags.length, `${handler} button count`).toBeGreaterThan(0);
|
||||
return tags;
|
||||
}
|
||||
|
||||
describe("offline Agent export transport contract", () => {
|
||||
it("routes preview and export through the Tauri desktop backend", () => {
|
||||
expect(functionBody(tauri, "previewAgentOfflineExport")).toContain('invoke("preview_agent_offline_export")');
|
||||
expect(functionBody(tauri, "exportAgentsOffline")).toContain('invoke("export_agents_offline", { path, driverKeys })');
|
||||
expect(tauriRegistry).toContain("commands::agents::preview_agent_offline_export,");
|
||||
expect(tauriRegistry).toContain("commands::agents::export_agents_offline,");
|
||||
expect(api).toContain('previewAgentOfflineExport = forward("previewAgentOfflineExport")');
|
||||
expect(api).toContain('exportAgentsOffline = forward("exportAgentsOffline")');
|
||||
});
|
||||
|
||||
it("keeps the Web transport explicitly unsupported without adding HTTP routes", () => {
|
||||
for (const operation of ["previewAgentOfflineExport", "exportAgentsOffline"]) {
|
||||
const body = functionBody(http, operation);
|
||||
expect(body).toContain("only available in the desktop app");
|
||||
expect(body).not.toContain("post(");
|
||||
expect(body).not.toContain("fetch(");
|
||||
}
|
||||
expect(webRegistry).not.toContain("agent-offline-export");
|
||||
expect(webRegistry).not.toContain("preview_agent_offline_export");
|
||||
expect(webRegistry).not.toContain("export_agents_offline");
|
||||
});
|
||||
|
||||
it("shows export only on Desktop while preserving offline import on Desktop and Web", () => {
|
||||
const exportButton = buttonTagByTestId("agent-offline-export-button");
|
||||
const importButton = buttonTagByTestId("agent-offline-import-button");
|
||||
expect(exportButton).toContain(`v-if="driverStoreTab === 'agent' && !isWeb"`);
|
||||
expect(importButton).toContain(`v-if="driverStoreTab === 'agent'"`);
|
||||
expect(importButton).not.toContain("!isWeb");
|
||||
expect(exportButton).toContain(':disabled="agentExportImportBlocked"');
|
||||
expect(importButton).toContain(':disabled="agentExportImportBlocked"');
|
||||
expect(driverStore.indexOf(exportButton)).toBeLessThan(driverStore.indexOf(importButton));
|
||||
});
|
||||
|
||||
it("shares one busy contract across export, import, driver, and JRE package actions", () => {
|
||||
const busyContract = sourceSection(driverStore, "const agentPackageBusy", "\n\nasync function openOfflineExportDialog");
|
||||
for (const dependency of ["agentImportBusy.value", "offlineExportLoading.value", "offlineExporting.value", "uninstallingDriver.value", "uninstallingJre.value", "installing.value", "preparingUpgradeAll.value", "upgradingAll.value", "reinstallingJre.value", "queuedDriverInstalls.value"]) {
|
||||
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 button of buttonTagsForClick(handler)) expect(button).toContain("agentPackageBusy");
|
||||
}
|
||||
|
||||
expect(sourceSection(driverStore, "async function importOfflineZip", "\n\nasync function importDriverFile")).toContain("if (agentExportImportBlocked.value) return;");
|
||||
});
|
||||
|
||||
it("uses the platform-specific ZIP save contract and keeps failed exports retryable", () => {
|
||||
const openDialog = sourceSection(driverStore, "async function openOfflineExportDialog", "\n\nasync function exportOfflinePackage");
|
||||
expect(openDialog).toContain("if (isWeb || agentExportImportBlocked.value) return;");
|
||||
expect(openDialog).toContain("offlineExportDialogOpen.value = true;");
|
||||
expect(openDialog.indexOf('offlineExportError.value = "";')).toBeLessThan(openDialog.indexOf("await api.previewAgentOfflineExport()"));
|
||||
|
||||
const exportPackage = sourceSection(driverStore, "async function exportOfflinePackage", "\n\nfunction chooseWebOfflineZip");
|
||||
expect(exportPackage).toContain("runAgentOfflineExportAction({");
|
||||
expect(exportPackage).toContain("defaultPath: `dbx-agents-offline-${platform}.zip`");
|
||||
expect(exportPackage).toContain('filters: [{ name: "ZIP", extensions: ["zip"] }]');
|
||||
|
||||
const success = sourceSection(exportPackage, "onSuccess:", "\n onError:");
|
||||
const failure = exportPackage.slice(exportPackage.indexOf("onError:"));
|
||||
expect(success).toContain("offlineExportDialogOpen.value = false;");
|
||||
expect(failure).toContain('t("driverStore.offlineExportFailed"');
|
||||
expect(failure).not.toContain("offlineExportDialogOpen.value = false;");
|
||||
|
||||
const dialog = sourceSection(driverStore, "<AgentOfflineExportDialog", " />");
|
||||
expect(dialog).toContain('v-model:open="offlineExportDialogOpen"');
|
||||
expect(dialog).toContain(':preview="offlineExportPreview"');
|
||||
expect(dialog).toContain(':loading="offlineExportLoading"');
|
||||
expect(dialog).toContain(':exporting="offlineExporting"');
|
||||
expect(dialog).toContain(':error="offlineExportError"');
|
||||
expect(dialog).toContain('@confirm="exportOfflinePackage"');
|
||||
});
|
||||
});
|
||||
@@ -118,6 +118,8 @@ export async function importAgentsFromZip(fileOrPath: string | File, operationId
|
||||
const backend = await getBackend();
|
||||
return backend.importAgentsFromZip(fileOrPath, operationId);
|
||||
}
|
||||
export const previewAgentOfflineExport = forward("previewAgentOfflineExport");
|
||||
export const exportAgentsOffline = forward("exportAgentsOffline");
|
||||
export const importAgentDriver = forward("importAgentDriver");
|
||||
export const importAgentJar = importAgentDriver;
|
||||
export async function reinstallJre(jreKey?: string, operationId?: string) {
|
||||
@@ -801,6 +803,11 @@ export type {
|
||||
AiConversation,
|
||||
PromptTemplate,
|
||||
AgentDriverInfo,
|
||||
AgentOfflineArtifactKind,
|
||||
AgentOfflineExportUnavailableReason,
|
||||
AgentOfflineExportCandidate,
|
||||
AgentOfflineExportPreview,
|
||||
AgentOfflineExportResult,
|
||||
DriverStoreUsage,
|
||||
DriverStoreUsageItem,
|
||||
DriverRuntimeHealth,
|
||||
|
||||
@@ -63,6 +63,8 @@ import type {
|
||||
DriverRuntimeSummary,
|
||||
UpgradeAllAgentDriversResult,
|
||||
AgentUpdateBlocker,
|
||||
AgentOfflineExportPreview,
|
||||
AgentOfflineExportResult,
|
||||
DesktopSettings,
|
||||
McpGlobalPolicy,
|
||||
SavedSqlSyncRequest,
|
||||
@@ -645,6 +647,14 @@ export async function importAgentsFromZip(fileOrPath: string | File, operationId
|
||||
return result.count;
|
||||
}
|
||||
|
||||
export async function previewAgentOfflineExport(): Promise<AgentOfflineExportPreview> {
|
||||
throw new Error("Offline Agent package export is only available in the desktop app.");
|
||||
}
|
||||
|
||||
export async function exportAgentsOffline(_path: string, _driverKeys: string[]): Promise<AgentOfflineExportResult> {
|
||||
throw new Error("Offline Agent package export is only available in the desktop app.");
|
||||
}
|
||||
|
||||
export async function importAgentDriver(dbType: string, pathOrFile: string | File): Promise<void> {
|
||||
let blob: Blob;
|
||||
let fileName: string;
|
||||
|
||||
@@ -141,6 +141,33 @@ export interface AgentUpdateBlocker {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export type AgentOfflineArtifactKind = "jar" | "native";
|
||||
|
||||
export type AgentOfflineExportUnavailableReason = "unmanagedInstall" | "localInstall" | "launchConfig" | "missingArtifact" | "invalidArtifact" | "unsafeSource" | "externalDriverRequired" | "missingManagedJre" | "invalidManagedJre";
|
||||
|
||||
export interface AgentOfflineExportCandidate {
|
||||
dbType: string;
|
||||
label: string;
|
||||
version: string;
|
||||
size: number;
|
||||
artifactKind: AgentOfflineArtifactKind | null;
|
||||
requiredJre: string | null;
|
||||
eligible: boolean;
|
||||
unavailableReason: AgentOfflineExportUnavailableReason | null;
|
||||
}
|
||||
|
||||
export interface AgentOfflineExportPreview {
|
||||
platform: string;
|
||||
candidates: AgentOfflineExportCandidate[];
|
||||
}
|
||||
|
||||
export interface AgentOfflineExportResult {
|
||||
platform: string;
|
||||
driverCount: number;
|
||||
jreCount: number;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
export type JavaRuntimeMode = "managed" | "system" | "custom";
|
||||
|
||||
export interface JavaRuntimeConfig {
|
||||
@@ -2028,6 +2055,14 @@ export async function importAgentsFromZip(path: string | File, operationId?: str
|
||||
return invoke("import_agents_from_zip", { path, operationId });
|
||||
}
|
||||
|
||||
export async function previewAgentOfflineExport(): Promise<AgentOfflineExportPreview> {
|
||||
return invoke("preview_agent_offline_export");
|
||||
}
|
||||
|
||||
export async function exportAgentsOffline(path: string, driverKeys: string[]): Promise<AgentOfflineExportResult> {
|
||||
return invoke("export_agents_offline", { path, driverKeys });
|
||||
}
|
||||
|
||||
export async function importAgentDriver(dbType: string, path: string | File): Promise<void> {
|
||||
if (typeof path !== "string") {
|
||||
throw new Error("Desktop driver import requires a local file path");
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { runAgentOfflineExportAction, runAgentOfflineExportFlow } from "@/lib/driverStore/agentOfflineExportFlow";
|
||||
|
||||
describe("runAgentOfflineExportFlow", () => {
|
||||
it("stops silently when destination selection is cancelled", async () => {
|
||||
const exportPackage = vi.fn();
|
||||
|
||||
await expect(
|
||||
runAgentOfflineExportFlow({
|
||||
driverKeys: ["duckdb"],
|
||||
chooseDestination: vi.fn().mockResolvedValue(null),
|
||||
exportPackage,
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
expect(exportPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("exports the selected drivers to the chosen destination", async () => {
|
||||
const driverKeys = ["duckdb", "redis"];
|
||||
const exportPackage = vi.fn().mockResolvedValue({ driverCount: 2 });
|
||||
|
||||
await expect(
|
||||
runAgentOfflineExportFlow({
|
||||
driverKeys,
|
||||
chooseDestination: vi.fn().mockResolvedValue("/tmp/dbx-agents.zip"),
|
||||
exportPackage,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
destination: "/tmp/dbx-agents.zip",
|
||||
result: { driverCount: 2 },
|
||||
});
|
||||
expect(exportPackage).toHaveBeenCalledWith("/tmp/dbx-agents.zip", ["duckdb", "redis"]);
|
||||
|
||||
driverKeys.push("kafka");
|
||||
expect(exportPackage.mock.calls[0]?.[1]).toEqual(["duckdb", "redis"]);
|
||||
});
|
||||
|
||||
it("does not open the save dialog for an empty selection", async () => {
|
||||
const chooseDestination = vi.fn();
|
||||
const exportPackage = vi.fn();
|
||||
|
||||
await expect(runAgentOfflineExportFlow({ driverKeys: [], chooseDestination, exportPackage })).resolves.toBeNull();
|
||||
expect(chooseDestination).not.toHaveBeenCalled();
|
||||
expect(exportPackage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["destination selection", vi.fn().mockRejectedValue(new Error("save failed"))],
|
||||
["package creation", vi.fn().mockResolvedValue("/tmp/dbx-agents.zip")],
|
||||
])("propagates %s failures to the caller", async (stage, chooseDestination) => {
|
||||
const exportPackage = stage === "package creation" ? vi.fn().mockRejectedValue(new Error("export failed")) : vi.fn();
|
||||
|
||||
await expect(
|
||||
runAgentOfflineExportFlow({
|
||||
driverKeys: ["duckdb"],
|
||||
chooseDestination,
|
||||
exportPackage,
|
||||
}),
|
||||
).rejects.toThrow(stage === "destination selection" ? "save failed" : "export failed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("runAgentOfflineExportAction", () => {
|
||||
function createOptions(overrides: Partial<Parameters<typeof runAgentOfflineExportAction<{ driverCount: number }>>[0]> = {}) {
|
||||
return {
|
||||
driverKeys: ["duckdb"],
|
||||
chooseDestination: vi.fn().mockResolvedValue("/tmp/dbx-agents.zip"),
|
||||
exportPackage: vi.fn().mockResolvedValue({ driverCount: 1 }),
|
||||
setBusy: vi.fn(),
|
||||
onSuccess: vi.fn(),
|
||||
onError: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
it("holds the busy state across destination selection and package creation", async () => {
|
||||
const events: string[] = [];
|
||||
const options = createOptions({
|
||||
setBusy: vi.fn((busy: boolean) => events.push(`busy:${busy}`)),
|
||||
chooseDestination: vi.fn(async () => {
|
||||
events.push("choose");
|
||||
return "/tmp/dbx-agents.zip";
|
||||
}),
|
||||
exportPackage: vi.fn(async () => {
|
||||
events.push("export");
|
||||
return { driverCount: 1 };
|
||||
}),
|
||||
onSuccess: vi.fn(() => events.push("success")),
|
||||
});
|
||||
|
||||
await expect(runAgentOfflineExportAction(options)).resolves.toBe("exported");
|
||||
expect(events).toEqual(["busy:true", "choose", "export", "success", "busy:false"]);
|
||||
expect(options.onError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps cancellation silent and always releases the busy state", async () => {
|
||||
const options = createOptions({ chooseDestination: vi.fn().mockResolvedValue(null) });
|
||||
|
||||
await expect(runAgentOfflineExportAction(options)).resolves.toBe("cancelled");
|
||||
expect(options.setBusy).toHaveBeenNthCalledWith(1, true);
|
||||
expect(options.setBusy).toHaveBeenLastCalledWith(false);
|
||||
expect(options.exportPackage).not.toHaveBeenCalled();
|
||||
expect(options.onSuccess).not.toHaveBeenCalled();
|
||||
expect(options.onError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["destination selection", { chooseDestination: vi.fn().mockRejectedValue(new Error("save failed")) }],
|
||||
["package creation", { exportPackage: vi.fn().mockRejectedValue(new Error("export failed")) }],
|
||||
])("reports %s failures and releases the busy state", async (_stage, overrides) => {
|
||||
const options = createOptions(overrides);
|
||||
|
||||
await expect(runAgentOfflineExportAction(options)).resolves.toBe("failed");
|
||||
expect(options.setBusy).toHaveBeenNthCalledWith(1, true);
|
||||
expect(options.setBusy).toHaveBeenLastCalledWith(false);
|
||||
expect(options.onSuccess).not.toHaveBeenCalled();
|
||||
expect(options.onError).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not enter the busy state for an empty selection", async () => {
|
||||
const options = createOptions({ driverKeys: [] });
|
||||
|
||||
await expect(runAgentOfflineExportAction(options)).resolves.toBe("empty");
|
||||
expect(options.setBusy).not.toHaveBeenCalled();
|
||||
expect(options.chooseDestination).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
export interface AgentOfflineExportFlowOptions<TResult> {
|
||||
driverKeys: string[];
|
||||
chooseDestination: () => Promise<string | null>;
|
||||
exportPackage: (destination: string, driverKeys: string[]) => Promise<TResult>;
|
||||
}
|
||||
|
||||
export interface AgentOfflineExportFlowResult<TResult> {
|
||||
destination: string;
|
||||
result: TResult;
|
||||
}
|
||||
|
||||
export interface AgentOfflineExportActionOptions<TResult> extends AgentOfflineExportFlowOptions<TResult> {
|
||||
setBusy: (busy: boolean) => void;
|
||||
onSuccess: (exported: AgentOfflineExportFlowResult<TResult>) => void | Promise<void>;
|
||||
onError: (error: unknown) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export type AgentOfflineExportActionResult = "empty" | "cancelled" | "exported" | "failed";
|
||||
|
||||
/**
|
||||
* Keep destination selection and package creation in one testable workflow.
|
||||
* The caller owns the busy state so it can cover the native save dialog too.
|
||||
*/
|
||||
export async function runAgentOfflineExportFlow<TResult>({ driverKeys, chooseDestination, exportPackage }: AgentOfflineExportFlowOptions<TResult>): Promise<AgentOfflineExportFlowResult<TResult> | null> {
|
||||
if (driverKeys.length === 0) return null;
|
||||
|
||||
const destination = await chooseDestination();
|
||||
if (!destination) return null;
|
||||
|
||||
return {
|
||||
destination,
|
||||
result: await exportPackage(destination, [...driverKeys]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Own the complete UI action lifecycle, including the native save dialog.
|
||||
* This keeps busy-state release and success/error side effects testable as one
|
||||
* contract instead of splitting them between the component and export helper.
|
||||
*/
|
||||
export async function runAgentOfflineExportAction<TResult>({ driverKeys, chooseDestination, exportPackage, setBusy, onSuccess, onError }: AgentOfflineExportActionOptions<TResult>): Promise<AgentOfflineExportActionResult> {
|
||||
if (driverKeys.length === 0) return "empty";
|
||||
|
||||
setBusy(true);
|
||||
try {
|
||||
const exported = await runAgentOfflineExportFlow({ driverKeys, chooseDestination, exportPackage });
|
||||
if (!exported) return "cancelled";
|
||||
|
||||
await onSuccess(exported);
|
||||
return "exported";
|
||||
} catch (error) {
|
||||
await onError(error);
|
||||
return "failed";
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user