Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions apps/desktop/public/connection-dialog-legacy.css
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,37 @@
}
}

@media (min-width: 1024px) {
.connection-db-picker-grid {
grid-template-columns: repeat(5, minmax(0, 1fr)) !important;
@supports (color: oklch(0.5 0.1 180)) {
@media (min-width: 1024px) {
.connection-db-picker-grid {
grid-template-columns: repeat(5, minmax(0, 1fr)) !important;
}
}
}

@supports not (color: oklch(0.5 0.1 180)) {
@media (min-width: 640px) {
.connection-db-picker-grid {
grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)) !important;
}

[data-slot="dialog-content"].connection-dialog-content--config {
width: calc(100vw - 2rem) !important;
min-width: 38rem !important;
height: 720px !important;
max-width: 880px !important;
}

[data-slot="dialog-content"].connection-dialog-content--config .connection-form-body {
align-content: start !important;
}

[data-slot="dialog-content"].connection-dialog-content--config .connection-url-params-row--compact {
align-items: center !important;
}

[data-slot="dialog-content"].connection-dialog-content--config .connection-url-params-row--with-hint .connection-url-params-label {
margin-top: 0.5rem !important;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const tablePatternsInput = ref("");
const expandedRunIds = reactive(new Set<string>());

const sqlConnections = computed(() => connectionStore.connections.filter((connection) => supportsScheduledDatabaseBackup(connection.db_type)));
const canCreateSchedule = computed(() => sqlConnections.value.length > 0);
const sortedRuns = computed(() => [...runs.value].sort((left, right) => Date.parse(right.startedAt) - Date.parse(left.startedAt)));
const weekdays = computed(() => [
{ value: 0, label: t("databaseBackup.weekdays.sunday") },
Expand Down Expand Up @@ -281,8 +282,9 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {
<div class="min-w-0">
<h3 class="text-base font-semibold">{{ t("databaseBackup.schedules") }}</h3>
<p class="mt-1 text-sm text-muted-foreground">{{ t("databaseBackup.runtimeRequirement") }}</p>
<p v-if="!canCreateSchedule" class="mt-1 text-xs text-muted-foreground">{{ t("databaseBackup.noSupportedConnections") }}</p>
</div>
<Button size="sm" :disabled="sqlConnections.length === 0" @click="openCreateSchedule">
<Button size="sm" :disabled="!canCreateSchedule" :title="canCreateSchedule ? t('databaseBackup.addSchedule') : t('databaseBackup.noSupportedConnections')" @click="openCreateSchedule">
<Plus class="mr-2 h-4 w-4" />
{{ t("databaseBackup.addSchedule") }}
</Button>
Expand Down Expand Up @@ -382,7 +384,7 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {
</div>

<Dialog v-model:open="scheduleDialogOpen">
<DialogContent class="max-h-[min(760px,calc(var(--dbx-viewport-height)-32px))] max-w-[min(720px,calc(100vw-32px))] overflow-y-auto">
<DialogContent class="dbx-form-dialog dbx-form-dialog--lg max-h-[min(760px,calc(var(--dbx-viewport-height)-32px))] max-w-[min(720px,calc(100vw-32px))] overflow-y-auto">
<DialogHeader>
<DialogTitle>{{ editingScheduleId ? t("databaseBackup.editSchedule") : t("databaseBackup.addSchedule") }}</DialogTitle>
</DialogHeader>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// @vitest-environment happy-dom

import { createApp, nextTick, type App } from "vue";
import { afterEach, describe, expect, it, vi } from "vitest";
import i18n from "@/i18n";
import ScheduledDatabaseBackupSettings from "@/components/backup/ScheduledDatabaseBackupSettings.vue";

const mocks = vi.hoisted(() => ({
connections: [] as Array<{ id: string; name: string; db_type: string }>,
ensureConnected: vi.fn(async () => {}),
listDatabases: vi.fn(async () => [{ name: "app" }]),
toast: vi.fn(),
saveSchedule: vi.fn(),
setScheduleEnabled: vi.fn(),
deleteSchedule: vi.fn(),
deleteRun: vi.fn(),
runSchedule: vi.fn(),
cancelRun: vi.fn(),
}));

vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: () => ({
connections: mocks.connections,
ensureConnected: mocks.ensureConnected,
getConfig: (connectionId: string) => mocks.connections.find((connection) => connection.id === connectionId),
sqlFileSource: null,
}),
}));

vi.mock("@/composables/useScheduledDatabaseBackups", () => ({
useScheduledDatabaseBackups: () => ({
schedules: { __v_isRef: true, value: [] },
runs: { __v_isRef: true, value: [] },
activeScheduleIds: new Set<string>(),
activeRunIds: new Set<string>(),
activeRuns: { __v_isRef: true, value: [] },
saveSchedule: mocks.saveSchedule,
setScheduleEnabled: mocks.setScheduleEnabled,
deleteSchedule: mocks.deleteSchedule,
deleteRun: mocks.deleteRun,
runSchedule: mocks.runSchedule,
cancelRun: mocks.cancelRun,
}),
}));

vi.mock("@/composables/useToast", () => ({
useToast: () => ({ toast: mocks.toast }),
}));

vi.mock("@/lib/backend/api", () => ({
listDatabases: mocks.listDatabases,
deleteDatabaseBackupFiles: vi.fn(),
revealPathInFileManager: vi.fn(),
}));

const mountedApps: App[] = [];

async function flush() {
await nextTick();
await new Promise((resolve) => setTimeout(resolve, 0));
await nextTick();
}

async function mountSettings() {
const container = document.createElement("div");
document.body.append(container);
const app = createApp(ScheduledDatabaseBackupSettings);
mountedApps.push(app);
app.use(i18n);
app.mount(container);
await flush();
}

function addScheduleButton(): HTMLButtonElement {
const label = String(i18n.global.t("databaseBackup.addSchedule"));
const button = Array.from(document.body.querySelectorAll("button")).find((item) => item.textContent?.includes(label));
if (!button) throw new Error("Add schedule button not found");
return button;
}

afterEach(() => {
for (const app of mountedApps.splice(0)) app.unmount();
document.body.innerHTML = "";
mocks.connections.splice(0);
mocks.ensureConnected.mockClear();
mocks.listDatabases.mockClear();
mocks.toast.mockClear();
});

describe("ScheduledDatabaseBackupSettings schedule dialog", () => {
it("opens the create schedule dialog for supported SQL connections", async () => {
mocks.connections.push({ id: "mysql-1", name: "Local MySQL", db_type: "mysql" });
await mountSettings();

const button = addScheduleButton();
expect(button.disabled).toBe(false);

button.click();
await flush();

const dialog = document.body.querySelector('[data-slot="dialog-content"]');
expect(dialog?.textContent).toContain(String(i18n.global.t("databaseBackup.addSchedule")));
expect(dialog?.textContent).toContain(String(i18n.global.t("databaseBackup.scheduleName")));
expect(mocks.ensureConnected).toHaveBeenCalledWith("mysql-1");
expect(mocks.listDatabases).toHaveBeenCalledWith("mysql-1");
});

it("disables create schedule when there are no supported backup connections", async () => {
mocks.connections.push({ id: "oracle-1", name: "Oracle", db_type: "oracle" });
await mountSettings();

expect(addScheduleButton().disabled).toBe(true);
expect(document.body.textContent).toContain(String(i18n.global.t("databaseBackup.noSupportedConnections")));
});
});
75 changes: 71 additions & 4 deletions apps/desktop/src/components/config/DriverStoreDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1475,7 +1475,7 @@ watch(driverStoreTab, (tab) => {
v-if="!driver.installed && !isPrestoSqlBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
size="sm"
variant="ghost"
class="h-7 w-7 rounded-md text-xs text-muted-foreground"
class="driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground"
:title="importingDriver === driver.db_type ? t('driverStore.importing') : t('driverStore.importLocalJar')"
:disabled="upgradingAll || installing !== null || agentImportBusy"
@click="importDriverFile(driver)"
Expand Down Expand Up @@ -1532,7 +1532,7 @@ watch(driverStoreTab, (tab) => {
</nav>

<!-- Driver list area -->
<div class="min-w-0 flex-1 overflow-y-auto sm:pl-4">
<div class="driver-store-agent-results min-w-0 flex-1 overflow-y-auto sm:pl-4">
<!-- Empty: still loading -->
<div v-if="drivers.length === 0" class="py-12 text-center text-sm text-muted-foreground">
{{ t("common.loading") }}
Expand Down Expand Up @@ -1593,7 +1593,7 @@ watch(driverStoreTab, (tab) => {
v-if="!driver.installed && !isPrestoSqlBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
size="sm"
variant="ghost"
class="h-7 w-7 rounded-md text-xs text-muted-foreground"
class="driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground"
:title="importingDriver === driver.db_type ? t('driverStore.importing') : t('driverStore.importLocalJar')"
:disabled="upgradingAll || installing !== null || agentImportBusy"
@click="importDriverFile(driver)"
Expand Down Expand Up @@ -1677,7 +1677,7 @@ watch(driverStoreTab, (tab) => {
v-if="!driver.installed && !isPrestoSqlBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
size="sm"
variant="ghost"
class="h-7 w-7 rounded-md text-xs text-muted-foreground"
class="driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground"
:title="importingDriver === driver.db_type ? t('driverStore.importing') : t('driverStore.importLocalJar')"
:disabled="upgradingAll || installing !== null || agentImportBusy"
@click="importDriverFile(driver)"
Expand Down Expand Up @@ -2139,6 +2139,73 @@ watch(driverStoreTab, (tab) => {
height: 2rem !important;
}

@supports not (color: oklch(0.5 0.1 180)) {
.driver-store-tab {
margin-top: 1.25rem !important;
}

.driver-store-agent-tab:not([hidden]),
.driver-store-jdbc-tab:not([hidden]),
.driver-store-storage-tab:not([hidden]) {
display: flex !important;
flex-direction: column !important;
gap: 1.25rem !important;
}

.driver-store-agent-tab > :not([hidden]) ~ :not([hidden]),
.driver-store-jdbc-tab > :not([hidden]) ~ :not([hidden]),
.driver-store-storage-tab > :not([hidden]) ~ :not([hidden]) {
margin-top: 0 !important;
}

.driver-store-local-import-button {
width: 2rem !important;
height: 2rem !important;
padding: 0 !important;
}

.driver-store-local-import-button svg {
width: 1rem !important;
height: 1rem !important;
}

@media (min-width: 640px) {
[data-driver-category-nav] {
width: 10rem !important;
flex-direction: column !important;
overflow-x: hidden !important;
overflow-y: auto !important;
border-right-width: 1px !important;
border-bottom-width: 0 !important;
padding-top: 0.125rem !important;
padding-right: 0.875rem !important;
padding-bottom: 0.125rem !important;
}

[data-driver-category-nav] > button {
width: 100% !important;
align-self: stretch !important;
}

[data-driver-category-nav] > button[aria-current="page"] {
background-color: rgba(23, 23, 23, 0.08) !important;
color: rgb(23, 23, 23) !important;
}

.dark [data-driver-category-nav] > button[aria-current="page"] {
background-color: rgba(255, 255, 255, 0.1) !important;
color: rgb(244, 244, 245) !important;
}

.driver-store-agent-results {
width: 0 !important;
min-width: 0 !important;
flex: 1 1 0% !important;
padding-left: 1rem !important;
}
}
}

@media (max-width: 900px) {
.driver-store-header {
align-items: flex-start !important;
Expand Down
31 changes: 26 additions & 5 deletions apps/desktop/src/components/connection/ConnectionDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2555,6 +2555,7 @@ const tlsCapableDatabaseTypes = new Set<DatabaseType>(["mysql", "starrocks", "po
const supportsTlsToggle = computed(() => tlsCapableDatabaseTypes.has(form.value.db_type));
const supportsCaCertificatePath = computed(() => form.value.db_type === "clickhouse");
const supportsGenericUrlParams = computed(() => form.value.db_type !== "manticoresearch" && form.value.db_type !== "hbase");
const showGenericUrlParamsHint = computed(() => form.value.db_type === "mysql" || form.value.db_type === "doris" || form.value.db_type === "starrocks");
const bareMysqlProfiles = new Set(["doris", "selectdb", "oceanbase"]);
const supportsMysqlTlsOptions = computed(() => form.value.db_type === "starrocks" || (form.value.db_type === "mysql" && !bareMysqlProfiles.has(selectedType.value)));
const supportsMysqlCleartextPasswordAuth = computed(() => form.value.db_type === "mysql" && !bareMysqlProfiles.has(selectedType.value));
Expand Down Expand Up @@ -6132,8 +6133,8 @@ function openExternalUrl(url: string) {
</label>
</div>

<div v-if="supportsGenericUrlParams" class="grid grid-cols-4 items-start gap-4">
<Label :class="connectionLabelClass">{{ t("connection.urlParams") }}</Label>
<div v-if="supportsGenericUrlParams" class="connection-url-params-row grid grid-cols-4 items-start gap-4" :class="{ 'connection-url-params-row--compact': !showGenericUrlParamsHint, 'connection-url-params-row--with-hint': showGenericUrlParamsHint }">
<Label :class="[connectionLabelClass, 'connection-url-params-label']">{{ t("connection.urlParams") }}</Label>
<div class="col-span-3 space-y-1.5">
<Input
v-model="form.url_params"
Expand All @@ -6157,7 +6158,7 @@ function openExternalUrl(url: string) {
: 'sslmode=prefer'
"
/>
<p v-if="form.db_type === 'mysql' || form.db_type === 'doris' || form.db_type === 'starrocks'" class="text-xs leading-5 text-muted-foreground">
<p v-if="showGenericUrlParamsHint" class="text-xs leading-5 text-muted-foreground">
{{ t("connection.localInfilePathHint") }}
</p>
</div>
Expand Down Expand Up @@ -6768,8 +6769,8 @@ function openExternalUrl(url: string) {
:key="hop.id"
type="button"
draggable="true"
class="flex min-h-10 items-center gap-2 rounded-md border px-2 text-left text-xs transition-colors"
:class="hop.id === selectedTransportLayer?.id ? 'border-primary bg-primary/5' : 'hover:bg-muted/50'"
class="connection-transport-layer-option flex min-h-10 items-center gap-2 rounded-md border px-2 text-left text-xs transition-colors"
:class="hop.id === selectedTransportLayer?.id ? 'connection-transport-layer-option--selected border-primary bg-primary/5' : 'hover:bg-muted/50'"
@click="selectedTransportLayerId = hop.id"
@dragstart="draggedTransportLayerId = hop.id"
@dragover.prevent
Expand Down Expand Up @@ -7295,6 +7296,16 @@ function openExternalUrl(url: string) {
background-color: rgba(23, 23, 23, 0.12) !important;
}

.connection-transport-layer-option--selected {
color: rgb(23, 23, 23) !important;
border-color: rgb(23, 23, 23) !important;
background-color: rgba(23, 23, 23, 0.08) !important;
}

.connection-transport-layer-option--selected:hover {
background-color: rgba(23, 23, 23, 0.12) !important;
}

.dark .connection-db-category-option--selected {
color: rgb(244, 244, 245) !important;
background-color: rgba(255, 255, 255, 0.1) !important;
Expand All @@ -7304,6 +7315,16 @@ function openExternalUrl(url: string) {
color: rgb(244, 244, 245) !important;
background-color: rgba(255, 255, 255, 0.14) !important;
}

.dark .connection-transport-layer-option--selected {
color: rgb(244, 244, 245) !important;
border-color: rgb(244, 244, 245) !important;
background-color: rgba(255, 255, 255, 0.1) !important;
}

.dark .connection-transport-layer-option--selected:hover {
background-color: rgba(255, 255, 255, 0.14) !important;
}
}

.connection-db-picker-option {
Expand Down
19 changes: 18 additions & 1 deletion apps/desktop/src/components/connection/TunnelProfileManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ async function testSelected() {
<p v-if="!draft.length" class="rounded-md border border-dashed px-3 py-4 text-center text-xs text-muted-foreground">
{{ t("settings.tunnelsEmpty") }}
</p>
<button v-for="profile in draft" :key="profile.id" type="button" class="flex min-h-10 items-center gap-2 rounded-md border px-3 text-left text-xs transition-colors" :class="profile.id === selectedId ? 'border-primary bg-primary/5' : 'hover:bg-muted/50'" @click="selectedId = profile.id">
<button v-for="profile in draft" :key="profile.id" type="button" class="flex min-h-10 items-center gap-2 rounded-md border px-3 text-left text-xs transition-colors" :class="profile.id === selectedId ? 'tunnel-profile-option--selected' : 'hover:bg-muted/50'" @click="selectedId = profile.id">
<span class="shrink-0 rounded border bg-muted/40 px-1.5 py-0.5 text-[10px] uppercase text-muted-foreground">{{ profileTypeLabel(profile) }}</span>
<span class="min-w-0 flex-1 truncate">{{ profileDisplayName(profile) }}</span>
<span class="min-w-0 truncate text-muted-foreground">{{ tunnelProfileSummary(profile) }}</span>
Expand Down Expand Up @@ -356,3 +356,20 @@ async function testSelected() {
</p>
</div>
</template>

<style>
.tunnel-profile-option--selected {
color: var(--foreground) !important;
border-color: var(--ring) !important;
background-color: var(--muted) !important;
box-shadow: inset 0 0 0 1px var(--border);
}

.tunnel-profile-option--selected:hover {
background-color: var(--accent) !important;
}

.tunnel-profile-option--selected .text-muted-foreground {
color: var(--muted-foreground) !important;
}
</style>
Loading