Skip to content
Merged
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
16 changes: 5 additions & 11 deletions apps/electron/main/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export function createStandaloneServerManager({ isDev, userDataPath, databasePat
if (pendingServerUrlPromise) return pendingServerUrlPromise;

if (isDev) {
cachedServerUrl = process.env.ELECTRON_START_URL ?? 'http://127.0.0.1:3000';
cachedServerUrl = process.env.ELECTRON_START_URL ?? 'http://localhost:3000';
return cachedServerUrl;
}

Expand Down Expand Up @@ -235,16 +235,9 @@ function readDesktopSecrets(filePath: string, logWarn: LogFn): Partial<Record<'B
function ensureDesktopSecrets(userDataPath: string, logWarn: LogFn): DesktopSecrets {
const secretsFilePath = path.join(userDataPath, DESKTOP_SECRETS_FILE_NAME);
const existingSecrets = readDesktopSecrets(secretsFilePath, logWarn);
const betterAuthSecret = isValidBase64Secret(existingSecrets.BETTER_AUTH_SECRET)
? existingSecrets.BETTER_AUTH_SECRET
: randomBytes(32).toString('base64');
const dsSecretKey = isValidBase64Secret(existingSecrets.DS_SECRET_KEY)
? existingSecrets.DS_SECRET_KEY
: randomBytes(32).toString('base64');
const shouldPersist =
betterAuthSecret !== existingSecrets.BETTER_AUTH_SECRET ||
dsSecretKey !== existingSecrets.DS_SECRET_KEY ||
!fs.existsSync(secretsFilePath);
const betterAuthSecret = isValidBase64Secret(existingSecrets.BETTER_AUTH_SECRET) ? existingSecrets.BETTER_AUTH_SECRET : randomBytes(32).toString('base64');
const dsSecretKey = isValidBase64Secret(existingSecrets.DS_SECRET_KEY) ? existingSecrets.DS_SECRET_KEY : randomBytes(32).toString('base64');
const shouldPersist = betterAuthSecret !== existingSecrets.BETTER_AUTH_SECRET || dsSecretKey !== existingSecrets.DS_SECRET_KEY || !fs.existsSync(secretsFilePath);

if (shouldPersist) {
try {
Expand Down Expand Up @@ -282,6 +275,7 @@ function createDesktopServerEnv(options: DesktopServerEnvOptions): NodeJS.Proces
HOSTNAME: options.hostname,
NODE_ENV: 'production',
PGLITE_DB_PATH: options.databasePath,
DORY_DEMO_RESOURCE_CACHE_DIR: path.join(options.userDataPath, 'demo-resources'),
BETTER_AUTH_SECRET: desktopSecrets.betterAuthSecret,
DS_SECRET_KEY: desktopSecrets.dsSecretKey,
NEXT_TELEMETRY_DISABLED: process.env.NEXT_TELEMETRY_DISABLED || '1',
Expand Down
9 changes: 9 additions & 0 deletions apps/electron/main/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ export function createMainWindow({ preloadPath, log }: CreateMainWindowOptions)

mainWindow.loadFile(loadingHtmlPath);

mainWindow.webContents.on('console-message', (_event, level, message, line, sourceId) => {
const levelName = level >= 3 ? 'error' : level === 2 ? 'warn' : 'info';
log(`[renderer:${levelName}]`, message, sourceId ? `${sourceId}:${line}` : '');
});

mainWindow.webContents.on('render-process-gone', (_event, details) => {
log('[electron] render process gone:', details.reason, details.exitCode);
});

mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ export default function VTable({
data-cell={`${r}-${colKeyName}`}
style={{ ...style, display: 'flex', alignItems: 'center', boxShadow: selectionEdgeShadow }}
className={cn(
'px-2 text-sm border-b border-r last:border-r-0 cursor-pointer outline-none select-none',
'px-2 text-sm border-b border-r cursor-pointer outline-none select-none',
'min-w-0 overflow-hidden',
isRowSelected && PRIMARY_SELECTION_SUBTLE_CLASS,
isCellSelected && PRIMARY_SELECTION_CLASS,
Expand Down
14 changes: 12 additions & 2 deletions apps/web/app/(app)/[organization]/connections/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,18 @@ export async function testConnection(params: CreateConnectionPayload & { timeout
return res;
}

export async function connectConnection(params: ConnectionListItem): Promise<ResponseObject<unknown>> {
const res = await fetchJsonResponse<unknown>(
type ConnectConnectionResult = {
connectionId?: string;
identityId?: string | null;
status?: string;
lastCheckStatus?: 'ok' | 'error' | 'unknown';
lastCheckAt?: string | null;
lastCheckLatencyMs?: number | null;
lastCheckError?: string | null;
};

export async function connectConnection(params: ConnectionListItem): Promise<ResponseObject<ConnectConnectionResult>> {
const res = await fetchJsonResponse<ConnectConnectionResult>(
'/api/connection/connect',
{
method: 'POST',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,11 @@ export function ConnectionDialog({

return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-2xl max-h-[95vh] flex flex-col" data-testid="connection-dialog">
<DialogContent
className="sm:max-w-2xl max-h-[95vh] flex flex-col"
data-testid="connection-dialog"
onPointerDownOutside={event => event.preventDefault()}
>
<DialogHeader className="shrink-0">
<DialogTitle>{isEditMode ? tc('Edit.title') : tc('Create.title')}</DialogTitle>
</DialogHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/regi
import { Input } from '@/registry/new-york-v4/ui/input';
import { Button } from '@/registry/new-york-v4/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/registry/new-york-v4/ui/select';
import { FieldHelp } from './shared';
import { applySelectedLocalDatabasePath, FieldHelp } from './shared';
import { isDesktopRuntime } from '@dory/shared/runtime';

type DuckDbMode = 'local' | 'motherduck';
Expand Down Expand Up @@ -155,10 +155,7 @@ export function DuckDbConnectionFields({ form }: { form: UseFormReturn<any> }) {
if (!selectedPath) {
return;
}
form.setValue('connection.path', selectedPath, {
shouldDirty: true,
shouldValidate: true,
});
applySelectedLocalDatabasePath(form, selectedPath);
}}
>
Choose
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/new-york-v4/
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/registry/new-york-v4/ui/form';
import { Input } from '@/registry/new-york-v4/ui/input';

export function inferLocalDatabaseTypeFromPath(filePath: string | null | undefined): 'duckdb' | 'sqlite' | null {
const extension = filePath
?.trim()
.match(/\.([^.\\/]+)$/)?.[1]
?.toLowerCase();
if (extension === 'duckdb') return 'duckdb';
if (extension === 'sqlite' || extension === 'sqlite3') return 'sqlite';
return null;
}

export function applySelectedLocalDatabasePath(form: UseFormReturn<any>, selectedPath: string) {
const inferredType = inferLocalDatabaseTypeFromPath(selectedPath);
if (inferredType === 'duckdb') {
form.setValue('connection.type', 'duckdb', { shouldDirty: true, shouldValidate: false });
form.setValue('connection.duckdbMode', 'local', { shouldDirty: true, shouldValidate: false });
form.setValue('connection.database', '', { shouldDirty: true, shouldValidate: false });
} else if (inferredType === 'sqlite') {
form.setValue('connection.type', 'sqlite', { shouldDirty: true, shouldValidate: false });
form.setValue('connection.database', 'main', { shouldDirty: true, shouldValidate: false });
}

form.setValue('connection.path', selectedPath, {
shouldDirty: true,
shouldValidate: true,
});
}

export function FieldHelp({ text }: { text: string }) {
return (
<Tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { UseFormReturn } from 'react-hook-form';
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/registry/new-york-v4/ui/form';
import { Input } from '@/registry/new-york-v4/ui/input';
import { Button } from '@/registry/new-york-v4/ui/button';
import { FieldHelp } from './shared';
import { applySelectedLocalDatabasePath, FieldHelp } from './shared';
import { useTranslations } from 'next-intl';
import { isDesktopRuntime } from '@dory/shared/runtime';

Expand Down Expand Up @@ -109,10 +109,7 @@ export function SqliteConnectionFields({ form }: { form: UseFormReturn<any> }) {
if (!selectedPath) {
return;
}
form.setValue('connection.path', selectedPath, {
shouldDirty: true,
shouldValidate: true,
});
applySelectedLocalDatabasePath(form, selectedPath);
}}
>
{t('Choose')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,11 @@ export function LocalFilesDialog({ open, onOpenChange, onSuccess, mode = 'create
const [advancedOpen, setAdvancedOpen] = useState(false);

const canPickFile = isDesktopRuntime() && typeof window !== 'undefined' && typeof window.electron?.selectLocalFile === 'function';
const isDesktop = isDesktopRuntime();
const relations = inspectResult?.relations ?? [];
const isEditMode = mode === 'edit';
const busy = inspecting || creating || loadingDataset;
const canSave = Boolean(inspectResult && datasetName.trim() && selectedRelations.size > 0 && (!isEditMode || datasetId));
const canSave = Boolean(datasetName.trim() && filePath.trim() && (!inspectResult || selectedRelations.size > 0) && (!isEditMode || datasetId));

const selectedRelationList = useMemo(() => {
return relations.filter(relation => selectedRelations.has(relation.relationName));
Expand Down Expand Up @@ -199,6 +200,11 @@ export function LocalFilesDialog({ open, onOpenChange, onSuccess, mode = 'create
if (!selectedPath) {
return;
}
setFilePath(selectedPath);
setDatasetName(current => current || defaultDatasetName(selectedPath));
setInspectResult(null);
setSelectedRelations(new Set());
setAdvancedOpen(false);
await handleInspect(selectedPath);
} catch (error: any) {
toast.error(error?.message ?? 'Failed to choose local file');
Expand Down Expand Up @@ -235,16 +241,36 @@ export function LocalFilesDialog({ open, onOpenChange, onSuccess, mode = 'create
};

const handleSave = async () => {
if (!inspectResult || !canSave) return;
if (!canSave) return;
setCreating(true);
try {
const trimmedPath = filePath.trim();
const activeInspectResult =
inspectResult?.source.path === trimmedPath
? inspectResult
: await inspectLocalFiles({
source: {
backend: 'serverPath',
filePath: trimmedPath,
},
}).then(result => {
if (!isSuccess(result) || !result.data) {
throw new Error(result.message || 'Failed to inspect local file');
}
return result.data;
});
const activeRelations = activeInspectResult.relations;
const activeSelectedRelations = inspectResult?.source.path === trimmedPath ? selectedRelationList : activeRelations;
if (activeSelectedRelations.length === 0) {
throw new Error('No supported sheets or tables were found in this file.');
}
const payload = {
name: datasetName.trim(),
source: {
backend: 'serverPath' as const,
filePath: filePath.trim(),
filePath: trimmedPath,
},
relations: selectedRelationList,
relations: activeSelectedRelations,
};
const result = isEditMode && datasetId ? await updateLocalFiles(datasetId, payload) : await createLocalFiles(payload);
if (!isSuccess(result)) {
Expand All @@ -263,7 +289,7 @@ export function LocalFilesDialog({ open, onOpenChange, onSuccess, mode = 'create

return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-2xl">
<DialogContent className="max-w-2xl" onPointerDownOutside={event => event.preventDefault()}>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
Expand All @@ -287,10 +313,12 @@ export function LocalFilesDialog({ open, onOpenChange, onSuccess, mode = 'create
Choose
</Button>
) : null}
<Button type="button" variant="secondary" onClick={() => handleInspect()} disabled={busy}>
{inspecting ? <Loader2 className="h-4 w-4 animate-spin" /> : <FileSearch className="h-4 w-4" />}
Preview
</Button>
{!isDesktop ? (
<Button type="button" variant="secondary" onClick={() => handleInspect()} disabled={busy}>
{inspecting ? <Loader2 className="h-4 w-4 animate-spin" /> : <FileSearch className="h-4 w-4" />}
Preview
</Button>
) : null}
</div>
</div>

Expand Down
19 changes: 19 additions & 0 deletions apps/web/app/(app)/[organization]/connections/form-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ function isAbsolutePath(value: string) {
return /^(\/|[a-zA-Z]:[\\/])/.test(value);
}

function getLowerPathExtension(value: string) {
const match = value.trim().match(/\.([^.\\/]+)$/);
return match?.[1]?.toLowerCase() ?? '';
}

export const ConnectionDialogFormSchema = z
.object({
connection: z.object({
Expand Down Expand Up @@ -55,6 +60,7 @@ export const ConnectionDialogFormSchema = z

if (value.connection.type === 'sqlite') {
const normalizedPath = value.connection.path?.trim() ?? '';
const extension = getLowerPathExtension(normalizedPath);
if (!normalizedPath) {
ctx.addIssue({
code: 'custom',
Expand All @@ -67,6 +73,12 @@ export const ConnectionDialogFormSchema = z
path: ['connection', 'path'],
message: 'SQLite path must be absolute',
});
} else if (extension === 'duckdb') {
ctx.addIssue({
code: 'custom',
path: ['connection', 'path'],
message: 'This is a DuckDB file. Change the connection type to DuckDB.',
});
}
return;
}
Expand All @@ -75,6 +87,7 @@ export const ConnectionDialogFormSchema = z
const mode = value.connection.duckdbMode === 'motherduck' ? 'motherduck' : 'local';
if (mode === 'local') {
const normalizedPath = value.connection.path?.trim() ?? '';
const extension = getLowerPathExtension(normalizedPath);
if (!normalizedPath) {
ctx.addIssue({
code: 'custom',
Expand All @@ -87,6 +100,12 @@ export const ConnectionDialogFormSchema = z
path: ['connection', 'path'],
message: 'DuckDB path must be absolute',
});
} else if (extension === 'sqlite' || extension === 'sqlite3') {
ctx.addIssue({
code: 'custom',
path: ['connection', 'path'],
message: 'This is a SQLite file. Change the connection type to SQLite.',
});
}
} else if (!value.identity.id && !value.identity.password?.trim()) {
ctx.addIssue({
Expand Down
Loading
Loading