From 5a20b8922225e56e80a4cb2807fb004a98bc9f17 Mon Sep 17 00:00:00 2001 From: Nuri Lacka Date: Fri, 7 Aug 2026 04:09:17 +0200 Subject: [PATCH 01/12] feat(iceberg): persistence & backend layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add src/types/iceberg.ts — full Iceberg type model - Extend src/types/backend.ts — icebergInstalled?, icebergInstances[] to DataBase - Add resources/python/iceberg_bridge.py — stdin/stdout JSON bridge (8 commands) Secrets injected via env vars, never in payload - Add IcebergDatalakeService — full CRUD, keytar credential management, pyiceberg installation, Python bridge runner (BE-03, BE-04) - Add icebergDatalake.ipcHandlers.ts — 13 lean IPC channels (BE-01) - Register handlers in ipcHandlers/index.ts and ipcSetup.ts - Add src/renderer/services/iceberg.service.ts — ipcRenderer wrappers - Add icebergDatalake.controller.ts — React Query hooks (6 queries, 5 mutations) --- resources/python/iceberg_bridge.py | 213 +++++ .../icebergDatalake.ipcHandlers.ts | 62 ++ src/main/ipcHandlers/index.ts | 2 + src/main/ipcSetup.ts | 2 + src/main/services/icebergDatalake.service.ts | 733 ++++++++++++++++++ .../controllers/icebergDatalake.controller.ts | 130 ++++ src/renderer/services/iceberg.service.ts | 103 +++ src/renderer/services/index.ts | 2 + src/types/backend.ts | 4 + src/types/iceberg.ts | 99 +++ 10 files changed, 1350 insertions(+) create mode 100644 resources/python/iceberg_bridge.py create mode 100644 src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts create mode 100644 src/main/services/icebergDatalake.service.ts create mode 100644 src/renderer/controllers/icebergDatalake.controller.ts create mode 100644 src/renderer/services/iceberg.service.ts create mode 100644 src/types/iceberg.ts diff --git a/resources/python/iceberg_bridge.py b/resources/python/iceberg_bridge.py new file mode 100644 index 00000000..b3275032 --- /dev/null +++ b/resources/python/iceberg_bridge.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +""" +Iceberg Bridge Script — DBT Studio +Reads one JSON command from stdin, writes one JSON result to stdout. +All errors are returned as {"ok": false, "error": "..."} — never thrown to stderr. + +Supported commands: + install_check, test_connection, list_namespaces, list_tables, + get_schema, get_snapshots, preview_table, create_metadata_file +""" + +import json +import os +import sys + + +def resolve_env_vars(props: dict) -> dict: + """Replace __ENV:VARNAME__ placeholders with actual environment variable values.""" + result = {} + for k, v in props.items(): + if isinstance(v, str) and v.startswith("__ENV:"): + env_key = v[6:] + result[k] = os.environ.get(env_key, "") + else: + result[k] = v + return result + + +def handle_install_check(_cmd: dict) -> dict: + """Check if pyiceberg is installed and return its version.""" + try: + import pyiceberg # noqa: PLC0415 + return {"ok": True, "installed": True, "version": pyiceberg.__version__} + except ImportError: + return {"ok": True, "installed": False} + + +def handle_test_connection(cmd: dict) -> dict: + """Test that the catalog is reachable by listing its top-level namespaces.""" + try: + from pyiceberg.catalog import load_catalog # noqa: PLC0415 + props = resolve_env_vars(cmd.get("catalog_properties", {})) + catalog = load_catalog(cmd["catalog_name"], **props) + namespaces = catalog.list_namespaces() + return {"ok": True, "namespace_count": len(namespaces)} + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + +def handle_list_namespaces(cmd: dict) -> dict: + """List namespaces, optionally under a parent namespace.""" + try: + from pyiceberg.catalog import load_catalog # noqa: PLC0415 + props = resolve_env_vars(cmd.get("catalog_properties", {})) + catalog = load_catalog(cmd["catalog_name"], **props) + parent = tuple(cmd["parent"]) if cmd.get("parent") else () + namespaces = catalog.list_namespaces(parent) + return {"ok": True, "namespaces": [list(ns) for ns in namespaces]} + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + +def handle_list_tables(cmd: dict) -> dict: + """List tables within a given namespace.""" + try: + from pyiceberg.catalog import load_catalog # noqa: PLC0415 + props = resolve_env_vars(cmd.get("catalog_properties", {})) + catalog = load_catalog(cmd["catalog_name"], **props) + namespace = tuple(cmd["namespace"]) + identifiers = catalog.list_tables(namespace) + # Each identifier is a tuple; return just the table name part + return {"ok": True, "tables": [ident[-1] for ident in identifiers]} + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + +def handle_get_schema(cmd: dict) -> dict: + """Return the current schema (field list) for a table.""" + try: + from pyiceberg.catalog import load_catalog # noqa: PLC0415 + props = resolve_env_vars(cmd.get("catalog_properties", {})) + catalog = load_catalog(cmd["catalog_name"], **props) + namespace = tuple(cmd["namespace"]) + table = catalog.load_table((*namespace, cmd["table"])) + schema = table.schema() + fields = [] + for field in schema.fields: + fields.append({ + "fieldId": field.field_id, + "name": field.name, + "type": str(field.field_type), + "required": field.required, + "doc": field.doc, + }) + return {"ok": True, "fields": fields} + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + +def handle_get_snapshots(cmd: dict) -> dict: + """Return the snapshot history for a table.""" + try: + from pyiceberg.catalog import load_catalog # noqa: PLC0415 + props = resolve_env_vars(cmd.get("catalog_properties", {})) + catalog = load_catalog(cmd["catalog_name"], **props) + namespace = tuple(cmd["namespace"]) + table = catalog.load_table((*namespace, cmd["table"])) + snapshots = [] + for snap in table.metadata.snapshots: + snapshots.append({ + "snapshotId": str(snap.snapshot_id), + "parentId": str(snap.parent_snapshot_id) if snap.parent_snapshot_id else None, + "operation": snap.summary.operation.value if snap.summary and snap.summary.operation else "unknown", + "committedAt": str(snap.timestamp_ms), + "manifestList": snap.manifest_list or "", + "summary": dict(snap.summary.additional_properties) if snap.summary else {}, + }) + return {"ok": True, "snapshots": snapshots} + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + +def handle_preview_table(cmd: dict) -> dict: + """Preview rows from a table using PyArrow scan.""" + try: + from pyiceberg.catalog import load_catalog # noqa: PLC0415 + props = resolve_env_vars(cmd.get("catalog_properties", {})) + catalog = load_catalog(cmd["catalog_name"], **props) + namespace = tuple(cmd["namespace"]) + table = catalog.load_table((*namespace, cmd["table"])) + limit = int(cmd.get("limit", 100)) + row_filter = cmd.get("row_filter") + + scan_kwargs = {"limit": limit} + if row_filter: + scan_kwargs["row_filter"] = row_filter + + arrow_table = table.scan(**scan_kwargs).to_arrow() + columns = arrow_table.schema.names + rows = arrow_table.to_pydict() + # Convert column-oriented dict to row-oriented list of lists + row_list = [ + [rows[col][i] for col in columns] + for i in range(len(arrow_table)) + ] + return { + "ok": True, + "columns": columns, + "rows": row_list, + "total": len(row_list), + } + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + +def handle_create_metadata_file(cmd: dict) -> dict: + """ + Initialize a local file-based Iceberg catalog at the given warehouse path. + Creates the metadata directory and writes a minimal catalog metadata JSON. + """ + try: + import uuid # noqa: PLC0415 + warehouse_path = cmd["warehouse_path"] + metadata_dir = os.path.join(warehouse_path, "metadata") + os.makedirs(metadata_dir, exist_ok=True) + metadata_path = os.path.join(metadata_dir, "v1.metadata.json") + if not os.path.exists(metadata_path): + catalog_metadata = { + "format-version": 1, + "table-uuid": str(uuid.uuid4()), + "location": warehouse_path, + "last-updated-ms": 0, + "last-column-id": 0, + "schema": {"type": "struct", "fields": []}, + "partition-spec": [], + "properties": {}, + "current-snapshot-id": -1, + "snapshots": [], + "snapshot-log": [], + "metadata-log": [], + } + with open(metadata_path, "w", encoding="utf-8") as f: + json.dump(catalog_metadata, f, indent=2) + return {"ok": True, "metadata_path": metadata_path} + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + +HANDLERS = { + "install_check": handle_install_check, + "test_connection": handle_test_connection, + "list_namespaces": handle_list_namespaces, + "list_tables": handle_list_tables, + "get_schema": handle_get_schema, + "get_snapshots": handle_get_snapshots, + "preview_table": handle_preview_table, + "create_metadata_file": handle_create_metadata_file, +} + + +if __name__ == "__main__": + try: + raw = sys.stdin.read() + cmd = json.loads(raw) + command_name = cmd.get("command") + handler = HANDLERS.get(command_name) + if handler is None: + print(json.dumps({"ok": False, "error": f"Unknown command: {command_name}"})) + else: + result = handler(cmd) + print(json.dumps(result)) + except Exception as e: # noqa: BLE001 + print(json.dumps({"ok": False, "error": str(e)})) diff --git a/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts b/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts new file mode 100644 index 00000000..27dbf884 --- /dev/null +++ b/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts @@ -0,0 +1,62 @@ +/** + * Iceberg Datalake IPC Handlers + * Lean wrappers — no business logic, no try/catch, pure delegation. + * Follows BE-01: IPC handlers are thin wrappers with zero business logic. + */ + +import { ipcMain } from 'electron'; +import { IcebergDatalakeService } from '../services/icebergDatalake.service'; + +export const registerIcebergDatalakeHandlers = () => { + ipcMain.handle('iceberg:list', () => IcebergDatalakeService.listInstances()); + + ipcMain.handle('iceberg:get', (_e, id: string) => + IcebergDatalakeService.getInstance(id), + ); + + ipcMain.handle('iceberg:create', (_e, data) => + IcebergDatalakeService.createInstance(data), + ); + + ipcMain.handle('iceberg:update', (_e, id: string, data) => + IcebergDatalakeService.updateInstance(id, data), + ); + + ipcMain.handle('iceberg:delete', (_e, id: string) => + IcebergDatalakeService.deleteInstance(id), + ); + + ipcMain.handle('iceberg:testCatalog', (_e, params) => + IcebergDatalakeService.testCatalogConnection(params), + ); + + ipcMain.handle('iceberg:listNamespaces', (_e, id: string, parent?) => + IcebergDatalakeService.listNamespaces(id, parent), + ); + + ipcMain.handle('iceberg:listTables', (_e, id: string, namespace) => + IcebergDatalakeService.listTables(id, namespace), + ); + + ipcMain.handle('iceberg:getSchema', (_e, id: string, namespace, table) => + IcebergDatalakeService.getTableSchema(id, namespace, table), + ); + + ipcMain.handle('iceberg:getSnapshots', (_e, id: string, namespace, table) => + IcebergDatalakeService.getTableSnapshots(id, namespace, table), + ); + + ipcMain.handle( + 'iceberg:previewTable', + (_e, id: string, namespace, table, limit, filter?) => + IcebergDatalakeService.previewTable(id, namespace, table, limit, filter), + ); + + ipcMain.handle('iceberg:createMetadataFile', (_e, warehousePath: string) => + IcebergDatalakeService.createMetadataFile(warehousePath), + ); + + ipcMain.handle('iceberg:ensureInstalled', () => + IcebergDatalakeService.ensurePyicebergInstalled(), + ); +}; diff --git a/src/main/ipcHandlers/index.ts b/src/main/ipcHandlers/index.ts index 32e95b83..d057d6eb 100644 --- a/src/main/ipcHandlers/index.ts +++ b/src/main/ipcHandlers/index.ts @@ -24,6 +24,7 @@ import registerFlowfileHandlers from './flowfile.ipcHandlers'; import { registerPipelineTemplatesHandlers } from './pipelineTemplates.ipcHandlers'; import registerTaskManagerHandlers from './taskManager.ipcHandlers'; import { registerSecondBrainHandlers } from './secondBrain.ipcHandlers'; +import { registerIcebergDatalakeHandlers } from './icebergDatalake.ipcHandlers'; export { registerCliHandlers, @@ -52,4 +53,5 @@ export { registerPipelineTemplatesHandlers, registerTaskManagerHandlers, registerSecondBrainHandlers, + registerIcebergDatalakeHandlers, }; diff --git a/src/main/ipcSetup.ts b/src/main/ipcSetup.ts index 70d8f2fb..530a55b0 100644 --- a/src/main/ipcSetup.ts +++ b/src/main/ipcSetup.ts @@ -26,6 +26,7 @@ import { registerPipelineTemplatesHandlers, registerTaskManagerHandlers, registerSecondBrainHandlers, + registerIcebergDatalakeHandlers, } from './ipcHandlers'; import { installIpcErrorHandling } from './utils/ipcErrorHandler'; @@ -57,6 +58,7 @@ const registerHandlers = (mainWindow: BrowserWindow) => { registerPipelineTemplatesHandlers(); registerTaskManagerHandlers(mainWindow); registerSecondBrainHandlers(); + registerIcebergDatalakeHandlers(); }; export default registerHandlers; diff --git a/src/main/services/icebergDatalake.service.ts b/src/main/services/icebergDatalake.service.ts new file mode 100644 index 00000000..b6603009 --- /dev/null +++ b/src/main/services/icebergDatalake.service.ts @@ -0,0 +1,733 @@ +/** + * IcebergDatalakeService + * Main backend service for Iceberg Data Lake instance management. + * Handles CRUD, secure credential storage, Python bridge invocation, + * and pyiceberg installation. + * + * Follows BE-03 (one cohesive service), BE-04 (no await inside Promise constructor). + */ + +import { v4 as uuidv4 } from 'uuid'; +import * as path from 'path'; +import { spawn } from 'child_process'; +import { app } from 'electron'; + +import { loadDatabaseFile, updateDatabase } from '../utils/fileHelper'; +import secureStorage from './secureStorage.service'; +import SettingsService from './settings.service'; + +import type { + IcebergInstanceConfig, + IcebergInstanceListItem, + CreateIcebergInstanceDTO, + UpdateIcebergInstanceDTO, + IcebergTestCatalogParams, + IcebergTestResult, + IcebergFieldSpec, + IcebergSnapshotInfo, + IcebergPreviewResult, +} from '../../types/iceberg'; +import type { CloudConnection } from '../../types/frontend'; + +export class IcebergDatalakeService { + // ───────────────────────────────────────────── + // Private: persistence helpers + // ───────────────────────────────────────────── + + private static async readInstances(): Promise { + try { + const db = await loadDatabaseFile(); + return db.icebergInstances ?? []; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] readInstances error:', error); + return []; + } + } + + private static async writeInstances( + instances: IcebergInstanceConfig[], + ): Promise { + await updateDatabase('icebergInstances', instances); + } + + // ───────────────────────────────────────────── + // Private: Python bridge helpers + // ───────────────────────────────────────────── + + private static getBridgePath(): string { + if (app.isPackaged) { + return path.join(process.resourcesPath, 'python', 'iceberg_bridge.py'); + } + return path.join( + __dirname, + '..', + '..', + '..', + 'resources', + 'python', + 'iceberg_bridge.py', + ); + } + + private static async getPythonPath(): Promise { + try { + const settings = await SettingsService.loadSettings(); + if (settings.pythonPath) return settings.pythonPath; + } catch { + // fall through to default + } + return 'python3'; + } + + /** + * Spawns the Python bridge, writes command JSON to stdin, reads result from stdout. + * BE-04: all async values resolved BEFORE entering new Promise constructor. + */ + private static async runBridge( + command: object, + env: Record = {}, + ): Promise { + const pythonPath = await IcebergDatalakeService.getPythonPath(); + const bridgePath = IcebergDatalakeService.getBridgePath(); + + return new Promise((resolve, reject) => { + const child = spawn(pythonPath, [bridgePath], { + env: { ...process.env, ...env }, + }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (d: Buffer) => { + stdout += d.toString(); + }); + child.stderr.on('data', (d: Buffer) => { + stderr += d.toString(); + }); + child.stdin.write(JSON.stringify(command)); + child.stdin.end(); + child.on('close', (code: number) => { + try { + const result = JSON.parse(stdout) as Record; + if (!result.ok) { + reject(new Error((result.error as string) ?? 'Bridge error')); + } else { + resolve(result); + } + } catch { + reject(new Error(`Bridge parse error (exit ${code}): ${stderr}`)); + } + }); + child.on('error', (err: Error) => reject(err)); + }); + } + + // ───────────────────────────────────────────── + // Private: catalog properties builder + // ───────────────────────────────────────────── + + /** + * Builds the catalog properties object and env map from an instance config. + * Secrets are passed via env vars using the __ENV:VARNAME__ placeholder pattern. + */ + private static async buildCatalogProperties( + instance: IcebergInstanceConfig, + ): Promise<{ props: Record; env: Record }> { + const props: Record = {}; + const env: Record = {}; + + switch (instance.catalogType) { + case 'file': + props.type = 'rest'; + if (instance.catalogPath) props.uri = instance.catalogPath; + break; + + case 'polaris': + case 'hive': + case 'sql': + props.type = 'rest'; + if (instance.endpoint) props.uri = instance.endpoint; + if (instance.catalogName) props.warehouse = instance.catalogName; + // Access token via env var + if (instance.catalogAccessTokenKey) { + try { + const token = await secureStorage.getCredential( + instance.catalogAccessTokenKey, + ); + if (token) { + props.token = '__ENV:ICEBERG_ACCESS_TOKEN__'; + // eslint-disable-next-line dot-notation + env['ICEBERG_ACCESS_TOKEN__'] = token; + } + } catch (tokenError) { + // eslint-disable-next-line no-console + console.error( + '[IcebergDatalakeService] token retrieval error:', + tokenError, + ); + } + } + break; + + case 'glue': + props.type = 'glue'; + if (instance.catalogName) props['glue.database'] = instance.catalogName; + break; + + case 'dynamodb': + props.type = 'dynamodb'; + if (instance.catalogName) + props['dynamodb.table-name'] = instance.catalogName; + break; + + case 'bigquery': + props.type = 'bigquery'; + if (instance.catalogName) props.gcp_project = instance.catalogName; + break; + + case 'in-memory': + props.type = 'in-memory'; + break; + + default: + break; + } + + // Storage credentials via Cloud Explorer connection + if (instance.storageConnectionId) { + try { + const db = await loadDatabaseFile(); + const conn: CloudConnection | undefined = (db.sources ?? []).find( + (s) => s.id === instance.storageConnectionId, + ); + if (conn) { + const { provider, config, id: connId } = conn; + + // Non-secret config fields are safe to read from the persisted config object. + // Secrets (secretAccessKey, accountKey, credentials JSON) are stored in keytar + // under provider-specific keys — matching the pattern used by DuckLake.service.ts. + const cfg = config as unknown as Record; + + const s3LikeProviders = [ + 'aws', + 'minio', + 'cloudflare-r2', + 'backblaze-b2', + 'rustfs', + 'garage', + ]; + + if (s3LikeProviders.includes(provider)) { + // Non-secret fields from config + if (cfg.endpoint) props['s3.endpoint'] = cfg.endpoint; + if (cfg.region) props['s3.region'] = cfg.region; + if (cfg.accessKeyId) { + props['s3.access-key-id'] = cfg.accessKeyId; + } + // Secret key from keytar: cloud-{provider}-{connectionId} + const secretKey = await secureStorage.getCredential( + `cloud-${provider}-${connId}`, + ); + if (secretKey) { + props['s3.secret-access-key'] = '__ENV:ICEBERG_S3_SECRET__'; + // eslint-disable-next-line dot-notation + env['ICEBERG_S3_SECRET__'] = secretKey; + } + // Optional session token (AWS only) + if (provider === 'aws') { + const sessionToken = await secureStorage.getCredential( + `cloud-aws-session-${connId}`, + ); + if (sessionToken) { + props['s3.session-token'] = '__ENV:ICEBERG_S3_SESSION__'; + // eslint-disable-next-line dot-notation + env['ICEBERG_S3_SESSION__'] = sessionToken; + } + } + } else if (provider === 'azure') { + if (cfg.accountName) props['adls.account-name'] = cfg.accountName; + // Secret from keytar: cloud-azure-{connectionId} + const accountKey = await secureStorage.getCredential( + `cloud-azure-${connId}`, + ); + if (accountKey) { + props['adls.account-key'] = '__ENV:ICEBERG_ADLS_KEY__'; + // eslint-disable-next-line dot-notation + env['ICEBERG_ADLS_KEY__'] = accountKey; + } + } else if (provider === 'gcs') { + if (cfg.projectId) props['gcs.project-id'] = cfg.projectId; + // GCS credentials JSON from keytar: cloud-gcs-{connectionId} + const gcsCreds = await secureStorage.getCredential( + `cloud-gcs-${connId}`, + ); + if (gcsCreds) { + props['gcs.credentials'] = '__ENV:ICEBERG_GCS_CREDS__'; + // eslint-disable-next-line dot-notation + env['ICEBERG_GCS_CREDS__'] = gcsCreds; + } + } + } + } catch (connError) { + // eslint-disable-next-line no-console + console.error( + '[IcebergDatalakeService] cloud connection lookup error:', + connError, + ); + } + } + + if (instance.storageBucket) props['s3.bucket'] = instance.storageBucket; + if (instance.storagePrefix) props['s3.prefix'] = instance.storagePrefix; + + return { props, env }; + } + + // ───────────────────────────────────────────── + // Public: CRUD + // ───────────────────────────────────────────── + + static async listInstances(): Promise { + try { + const instances = await IcebergDatalakeService.readInstances(); + return instances.map( + ({ + id, + name, + description, + catalogType, + storageType, + createdAt, + updatedAt, + }) => ({ + id, + name, + description, + catalogType, + storageType, + createdAt, + updatedAt, + }), + ); + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] listInstances error:', error); + throw error; + } + } + + static async getInstance(id: string): Promise { + try { + const instances = await IcebergDatalakeService.readInstances(); + const instance = instances.find((i) => i.id === id); + if (!instance) throw new Error(`Iceberg instance not found: ${id}`); + return instance; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] getInstance error:', error); + throw error; + } + } + + static async createInstance( + data: CreateIcebergInstanceDTO, + ): Promise { + try { + const id = uuidv4(); + const now = new Date().toISOString(); + + let catalogAccessTokenKey: string | undefined; + if (data.accessToken) { + catalogAccessTokenKey = `iceberg-catalog-token-${id}`; + await secureStorage.setCredential( + catalogAccessTokenKey, + data.accessToken, + ); + } + + // Strip the raw accessToken before persisting + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { accessToken: _accessTokenCreate, ...rest } = data; + + const newInstance: IcebergInstanceConfig = { + ...rest, + id, + catalogAccessTokenKey: + catalogAccessTokenKey ?? data.catalogAccessTokenKey, + createdAt: now, + updatedAt: now, + }; + + const instances = await IcebergDatalakeService.readInstances(); + instances.push(newInstance); + await IcebergDatalakeService.writeInstances(instances); + + return newInstance; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] createInstance error:', error); + throw error; + } + } + + static async updateInstance( + id: string, + data: UpdateIcebergInstanceDTO, + ): Promise { + try { + const instances = await IcebergDatalakeService.readInstances(); + const idx = instances.findIndex((i) => i.id === id); + if (idx < 0) throw new Error(`Iceberg instance not found: ${id}`); + + // Handle access token update + if (data.accessToken) { + const key = + instances[idx].catalogAccessTokenKey ?? `iceberg-catalog-token-${id}`; + await secureStorage.setCredential(key, data.accessToken); + instances[idx].catalogAccessTokenKey = key; + } + + // Strip the raw accessToken before persisting + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { accessToken: _accessTokenUpdate, ...rest } = data; + + instances[idx] = { + ...instances[idx], + ...rest, + id, + updatedAt: new Date().toISOString(), + }; + + await IcebergDatalakeService.writeInstances(instances); + return instances[idx]; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] updateInstance error:', error); + throw error; + } + } + + static async deleteInstance(id: string): Promise { + try { + const instances = await IcebergDatalakeService.readInstances(); + const instance = instances.find((i) => i.id === id); + if (!instance) throw new Error(`Iceberg instance not found: ${id}`); + + if (instance.catalogAccessTokenKey) { + try { + await secureStorage.deleteCredential(instance.catalogAccessTokenKey); + } catch (keyError) { + // eslint-disable-next-line no-console + console.error( + '[IcebergDatalakeService] keytar delete error:', + keyError, + ); + } + } + + const updated = instances.filter((i) => i.id !== id); + await IcebergDatalakeService.writeInstances(updated); + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] deleteInstance error:', error); + throw error; + } + } + + // ───────────────────────────────────────────── + // Public: connection testing + // ───────────────────────────────────────────── + + static async testCatalogConnection( + params: IcebergTestCatalogParams, + ): Promise { + try { + const props: Record = {}; + const env: Record = {}; + + if (params.endpoint) props.uri = params.endpoint; + if (params.catalogName) props.warehouse = params.catalogName; + if (params.catalogPath) props.uri = params.catalogPath; + + switch (params.catalogType) { + case 'file': + props.type = 'rest'; + break; + case 'polaris': + case 'hive': + case 'sql': + props.type = 'rest'; + if (params.accessToken) { + props.token = '__ENV:ICEBERG_ACCESS_TOKEN__'; + // eslint-disable-next-line dot-notation + env['ICEBERG_ACCESS_TOKEN__'] = params.accessToken; + } + break; + case 'glue': + props.type = 'glue'; + break; + case 'dynamodb': + props.type = 'dynamodb'; + break; + case 'bigquery': + props.type = 'bigquery'; + break; + case 'in-memory': + props.type = 'in-memory'; + break; + default: + props.type = 'rest'; + } + + await IcebergDatalakeService.runBridge( + { + command: 'test_connection', + catalog_name: params.catalogName ?? 'test', + catalog_properties: props, + }, + env, + ); + return { success: true }; + } catch (error) { + // eslint-disable-next-line no-console + console.error( + '[IcebergDatalakeService] testCatalogConnection error:', + error, + ); + return { success: false, error: String(error) }; + } + } + + // ───────────────────────────────────────────── + // Public: pyiceberg installation + // ───────────────────────────────────────────── + + static async ensurePyicebergInstalled(): Promise<{ + installed: boolean; + version?: string; + }> { + try { + const settings = await SettingsService.loadSettings(); + if (settings.icebergInstalled) { + return { installed: true, version: settings.icebergVersion }; + } + + // Check if already installed + const checkResult = (await IcebergDatalakeService.runBridge({ + command: 'install_check', + })) as Record; + + if (checkResult.installed) { + const version = checkResult.version as string | undefined; + await SettingsService.saveSettings({ + ...settings, + icebergInstalled: true, + icebergVersion: version, + }); + return { installed: true, version }; + } + + // Install pyiceberg with common extras + const pythonPath = await IcebergDatalakeService.getPythonPath(); + await new Promise((resolve, reject) => { + const child = spawn(pythonPath, [ + '-m', + 'pip', + 'install', + 'pyiceberg[s3fs,glue,hive,sql-sqlite,sql-postgres,pyarrow]', + '--quiet', + ]); + child.on('close', (code: number) => { + if (code === 0) resolve(); + else reject(new Error(`pip install failed with exit code ${code}`)); + }); + child.on('error', (err: Error) => reject(err)); + }); + + // Verify installation + const verifyResult = (await IcebergDatalakeService.runBridge({ + command: 'install_check', + })) as Record; + + if (verifyResult.installed) { + const version = verifyResult.version as string | undefined; + const currentSettings = await SettingsService.loadSettings(); + await SettingsService.saveSettings({ + ...currentSettings, + icebergInstalled: true, + icebergVersion: version, + }); + return { installed: true, version }; + } + + return { installed: false }; + } catch (error) { + // eslint-disable-next-line no-console + console.error( + '[IcebergDatalakeService] ensurePyicebergInstalled error:', + error, + ); + throw error; + } + } + + // ───────────────────────────────────────────── + // Public: table operations + // ───────────────────────────────────────────── + + static async listNamespaces( + id: string, + parent?: string[], + ): Promise { + try { + const instance = await IcebergDatalakeService.getInstance(id); + const { props, env } = + await IcebergDatalakeService.buildCatalogProperties(instance); + const result = (await IcebergDatalakeService.runBridge( + { + command: 'list_namespaces', + catalog_name: instance.catalogName ?? id, + catalog_properties: props, + parent: parent ?? [], + }, + env, + )) as Record; + return (result.namespaces as string[][]) ?? []; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] listNamespaces error:', error); + throw error; + } + } + + static async listTables(id: string, namespace: string[]): Promise { + try { + const instance = await IcebergDatalakeService.getInstance(id); + const { props, env } = + await IcebergDatalakeService.buildCatalogProperties(instance); + const result = (await IcebergDatalakeService.runBridge( + { + command: 'list_tables', + catalog_name: instance.catalogName ?? id, + catalog_properties: props, + namespace, + }, + env, + )) as Record; + return (result.tables as string[]) ?? []; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] listTables error:', error); + throw error; + } + } + + static async getTableSchema( + id: string, + namespace: string[], + table: string, + ): Promise { + try { + const instance = await IcebergDatalakeService.getInstance(id); + const { props, env } = + await IcebergDatalakeService.buildCatalogProperties(instance); + const result = (await IcebergDatalakeService.runBridge( + { + command: 'get_schema', + catalog_name: instance.catalogName ?? id, + catalog_properties: props, + namespace, + table, + }, + env, + )) as Record; + return (result.fields as IcebergFieldSpec[]) ?? []; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] getTableSchema error:', error); + throw error; + } + } + + static async getTableSnapshots( + id: string, + namespace: string[], + table: string, + ): Promise { + try { + const instance = await IcebergDatalakeService.getInstance(id); + const { props, env } = + await IcebergDatalakeService.buildCatalogProperties(instance); + const result = (await IcebergDatalakeService.runBridge( + { + command: 'get_snapshots', + catalog_name: instance.catalogName ?? id, + catalog_properties: props, + namespace, + table, + }, + env, + )) as Record; + return (result.snapshots as IcebergSnapshotInfo[]) ?? []; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] getTableSnapshots error:', error); + throw error; + } + } + + static async previewTable( + id: string, + namespace: string[], + table: string, + limit: number, + rowFilter?: string, + ): Promise { + try { + const instance = await IcebergDatalakeService.getInstance(id); + const { props, env } = + await IcebergDatalakeService.buildCatalogProperties(instance); + const result = (await IcebergDatalakeService.runBridge( + { + command: 'preview_table', + catalog_name: instance.catalogName ?? id, + catalog_properties: props, + namespace, + table, + limit: Math.min(Math.max(1, limit), 10000), // clamp per BE-02 + row_filter: rowFilter, + }, + env, + )) as Record; + return { + columns: (result.columns as string[]) ?? [], + rows: (result.rows as unknown[][]) ?? [], + total: result.total as number | undefined, + }; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] previewTable error:', error); + throw error; + } + } + + // ───────────────────────────────────────────── + // Public: file helpers + // ───────────────────────────────────────────── + + static async createMetadataFile(warehousePath: string): Promise { + try { + const result = (await IcebergDatalakeService.runBridge({ + command: 'create_metadata_file', + warehouse_path: warehousePath, + })) as Record; + return (result.metadata_path as string) ?? ''; + } catch (error) { + // eslint-disable-next-line no-console + console.error( + '[IcebergDatalakeService] createMetadataFile error:', + error, + ); + throw error; + } + } +} diff --git a/src/renderer/controllers/icebergDatalake.controller.ts b/src/renderer/controllers/icebergDatalake.controller.ts new file mode 100644 index 00000000..a76a5e9c --- /dev/null +++ b/src/renderer/controllers/icebergDatalake.controller.ts @@ -0,0 +1,130 @@ +/** + * Iceberg Datalake React Query Controller + * Provides typed query and mutation hooks for all Iceberg IPC operations. + * Follows the React Query v3 pattern used throughout the app. + */ + +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import type { + CreateIcebergInstanceDTO, + IcebergTestCatalogParams, +} from '../../types/iceberg'; +import * as icebergService from '../services/iceberg.service'; + +// ───────────────────────────────────────────── +// Queries +// ───────────────────────────────────────────── + +export const useListIcebergInstances = () => + useQuery(['iceberg', 'list'], icebergService.listIcebergInstances); + +export const useGetIcebergInstance = (id: string) => + useQuery( + ['iceberg', 'instance', id], + () => icebergService.getIcebergInstance(id), + { enabled: !!id }, + ); + +export const useListIcebergNamespaces = (id: string, parent?: string[]) => + useQuery( + ['iceberg', 'namespaces', id, parent ?? []], + () => icebergService.listIcebergNamespaces(id, parent), + { enabled: !!id }, + ); + +export const useListIcebergTables = (id: string, namespace: string[]) => + useQuery( + ['iceberg', 'tables', id, namespace], + () => icebergService.listIcebergTables(id, namespace), + { enabled: !!id && namespace.length > 0 }, + ); + +export const useGetIcebergSchema = ( + id: string, + namespace: string[], + table: string, +) => + useQuery( + ['iceberg', 'schema', id, namespace, table], + () => icebergService.getIcebergTableSchema(id, namespace, table), + { enabled: !!id && !!table }, + ); + +export const useGetIcebergSnapshots = ( + id: string, + namespace: string[], + table: string, +) => + useQuery( + ['iceberg', 'snapshots', id, namespace, table], + () => icebergService.getIcebergTableSnapshots(id, namespace, table), + { enabled: !!id && !!table }, + ); + +// ───────────────────────────────────────────── +// Mutations +// ───────────────────────────────────────────── + +export const useCreateIcebergInstance = () => { + const qc = useQueryClient(); + return useMutation( + (data: CreateIcebergInstanceDTO) => + icebergService.createIcebergInstance(data), + { + onSuccess: () => qc.invalidateQueries(['iceberg', 'list']), + }, + ); +}; + +export const useUpdateIcebergInstance = () => { + const qc = useQueryClient(); + return useMutation( + ({ id, data }: { id: string; data: Partial }) => + icebergService.updateIcebergInstance(id, data), + { + onSuccess: (_result, { id }) => { + qc.invalidateQueries(['iceberg', 'list']); + qc.invalidateQueries(['iceberg', 'instance', id]); + }, + }, + ); +}; + +export const useDeleteIcebergInstance = () => { + const qc = useQueryClient(); + return useMutation((id: string) => icebergService.deleteIcebergInstance(id), { + onSuccess: () => qc.invalidateQueries(['iceberg', 'list']), + }); +}; + +export const useTestIcebergCatalog = () => + useMutation((params: IcebergTestCatalogParams) => + icebergService.testIcebergCatalog(params), + ); + +export const usePreviewIcebergTable = () => + useMutation( + ({ + id, + namespace, + table, + limit, + rowFilter, + }: { + id: string; + namespace: string[]; + table: string; + limit: number; + rowFilter?: string; + }) => + icebergService.previewIcebergTable( + id, + namespace, + table, + limit, + rowFilter, + ), + ); + +export const useEnsureIcebergInstalled = () => + useMutation(() => icebergService.ensureIcebergInstalled()); diff --git a/src/renderer/services/iceberg.service.ts b/src/renderer/services/iceberg.service.ts new file mode 100644 index 00000000..8463242c --- /dev/null +++ b/src/renderer/services/iceberg.service.ts @@ -0,0 +1,103 @@ +/** + * Iceberg renderer service + * Named exports wrapping window.electron.ipcRenderer.invoke — no default exports. + * Follows the renderer service naming pattern (import * as icebergService). + */ + +import type { + CreateIcebergInstanceDTO, + IcebergInstanceListItem, + IcebergInstanceConfig, + IcebergTestCatalogParams, + IcebergTestResult, + IcebergFieldSpec, + IcebergSnapshotInfo, + IcebergPreviewResult, +} from '../../types/iceberg'; + +export const listIcebergInstances = (): Promise => + window.electron.ipcRenderer.invoke('iceberg:list'); + +export const getIcebergInstance = ( + id: string, +): Promise => + window.electron.ipcRenderer.invoke('iceberg:get', id); + +export const createIcebergInstance = ( + data: CreateIcebergInstanceDTO, +): Promise => + window.electron.ipcRenderer.invoke('iceberg:create', data); + +export const updateIcebergInstance = ( + id: string, + data: Partial, +): Promise => + window.electron.ipcRenderer.invoke('iceberg:update', id, data); + +export const deleteIcebergInstance = (id: string): Promise => + window.electron.ipcRenderer.invoke('iceberg:delete', id); + +export const testIcebergCatalog = ( + params: IcebergTestCatalogParams, +): Promise => + window.electron.ipcRenderer.invoke('iceberg:testCatalog', params); + +export const listIcebergNamespaces = ( + id: string, + parent?: string[], +): Promise => + window.electron.ipcRenderer.invoke('iceberg:listNamespaces', id, parent); + +export const listIcebergTables = ( + id: string, + namespace: string[], +): Promise => + window.electron.ipcRenderer.invoke('iceberg:listTables', id, namespace); + +export const getIcebergTableSchema = ( + id: string, + namespace: string[], + table: string, +): Promise => + window.electron.ipcRenderer.invoke('iceberg:getSchema', id, namespace, table); + +export const getIcebergTableSnapshots = ( + id: string, + namespace: string[], + table: string, +): Promise => + window.electron.ipcRenderer.invoke( + 'iceberg:getSnapshots', + id, + namespace, + table, + ); + +export const previewIcebergTable = ( + id: string, + namespace: string[], + table: string, + limit: number, + rowFilter?: string, +): Promise => + window.electron.ipcRenderer.invoke( + 'iceberg:previewTable', + id, + namespace, + table, + limit, + rowFilter, + ); + +export const createIcebergMetadataFile = ( + warehousePath: string, +): Promise => + window.electron.ipcRenderer.invoke( + 'iceberg:createMetadataFile', + warehousePath, + ); + +export const ensureIcebergInstalled = (): Promise<{ + installed: boolean; + version?: string; +}> => window.electron.ipcRenderer.invoke('iceberg:ensureInstalled'); diff --git a/src/renderer/services/index.ts b/src/renderer/services/index.ts index 86016633..599ee92a 100644 --- a/src/renderer/services/index.ts +++ b/src/renderer/services/index.ts @@ -13,6 +13,7 @@ import { DuckLakeService } from './duckLake.service'; import * as lineageService from './lineage.service'; import * as languageIntelligenceService from './languageIntelligence.service'; import * as agentService from './agent.service'; +import * as icebergService from './iceberg.service'; export { settingsServices, @@ -30,4 +31,5 @@ export { lineageService, languageIntelligenceService, agentService, + icebergService, }; diff --git a/src/types/backend.ts b/src/types/backend.ts index 639b23ae..db615a72 100644 --- a/src/types/backend.ts +++ b/src/types/backend.ts @@ -1,5 +1,6 @@ import { QueryResult } from 'pg'; import { CloudConnection, RecentItem } from './frontend'; +import type { IcebergInstanceConfig } from './iceberg'; export type SupportedConnectionTypes = | 'postgres' @@ -341,6 +342,8 @@ export type SettingsType = { flowfileVersion?: string; flowfilePort?: string; flowfileAutoStart?: string; + icebergInstalled?: boolean; + icebergVersion?: string; }; export type FileDialogProperties = 'openFile' | 'openDirectory'; @@ -363,6 +366,7 @@ export type DataBase = { connections: ConnectionModel[]; sources: CloudConnection[]; recentItems: RecentItem[]; + icebergInstances?: IcebergInstanceConfig[]; }; // Rosetta Version Management Types diff --git a/src/types/iceberg.ts b/src/types/iceberg.ts new file mode 100644 index 00000000..889885bf --- /dev/null +++ b/src/types/iceberg.ts @@ -0,0 +1,99 @@ +// src/types/iceberg.ts +// Iceberg Data Lake — Phase 1: TypeScript type definitions + +export type IcebergCatalogType = + | 'file' + | 'polaris' + | 'glue' + | 'hive' + | 'dynamodb' + | 'sql' + | 'bigquery' + | 'in-memory'; + +export type IcebergStorageType = 'local' | 'cloud'; + +export type IcebergCloudProvider = 's3-compatible' | 'aws' | 'azure' | 'gcs'; + +export interface IcebergInstanceConfig { + id: string; + name: string; + description?: string; + // Catalog + catalogType: IcebergCatalogType; + catalogPath?: string; // file-based: path to metadata.json + endpoint?: string; // REST: Polaris/Lakekeeper endpoint URL + catalogName?: string; // REST: catalog name or warehouse + catalogAccessTokenKey?: string; // keytar key: "iceberg-catalog-token-{id}" + catalogConnectionId?: string; // Cloud Explorer connectionId for vended credentials + catalogBucket?: string; + catalogPrefix?: string; + // Storage + storageType: IcebergStorageType; + localPath?: string; + cloudProvider?: IcebergCloudProvider; + storageConnectionId?: string; // Cloud Explorer connectionId for data files + storageBucket?: string; + storagePrefix?: string; + // Metadata + createdAt: string; + updatedAt: string; +} + +export interface IcebergInstanceListItem { + id: string; + name: string; + description?: string; + catalogType: IcebergCatalogType; + storageType: IcebergStorageType; + createdAt: string; + updatedAt: string; +} + +// DTOs +export type CreateIcebergInstanceDTO = Omit< + IcebergInstanceConfig, + 'id' | 'createdAt' | 'updatedAt' +> & { + accessToken?: string; // raw token — service stores in keytar, strips before saving +}; + +export type UpdateIcebergInstanceDTO = Partial; + +// Bridge types +export interface IcebergFieldSpec { + fieldId: number; + name: string; + type: string; + required: boolean; + doc?: string; +} + +export interface IcebergSnapshotInfo { + snapshotId: string; + parentId?: string; + operation: string; + committedAt: string; + manifestList: string; + summary: Record; +} + +export interface IcebergPreviewResult { + columns: string[]; + rows: unknown[][]; + total?: number; +} + +export interface IcebergTestResult { + success: boolean; + error?: string; +} + +export interface IcebergTestCatalogParams { + catalogType: IcebergCatalogType; + catalogPath?: string; + endpoint?: string; + catalogName?: string; + connectionId?: string; // resolves credentials from Cloud Explorer + accessToken?: string; // raw token (not stored yet at test time) +} From 56d0229bab151cd7f7254b72f8db4e6755190981 Mon Sep 17 00:00:00 2001 From: Nuri Lacka Date: Sat, 8 Aug 2026 20:00:41 +0200 Subject: [PATCH 02/12] feat(iceberg): add catalog persistence and connection wizard - add the four-step Iceberg create and edit wizard to the DataLake workflow - integrate Iceberg instances into the dashboard, unified instance list, sidebar, and URL-based detail routing - add create, edit, delete, loading, error, and pyiceberg installation flows - introduce canonical catalog types for SQLite, SQL, REST, Polaris, Hive, Hadoop, Glue, and Nessie - keep SQLite as a distinct user-facing catalog while mapping it to PyIceberg SQL internally - add a backend capability contract for enabled catalogs, required fields, authentication modes, and supported warehouse types - expose Iceberg capabilities through typed IPC, renderer services, and React Query controllers - reject disabled catalogs and invalid catalog/warehouse combinations in the trusted main process - replace the synthetic local metadata JSON with a durable SQLite PyIceberg catalog - initialize the local warehouse and default namespace through iceberg:createMetadataFile - verify local catalogs by closing and reloading them before returning namespace and table results - add a local sample-data generator and SQLite bridge persistence test - add PostgreSQL and Neon SQL Catalog support through existing database connections - persist only the selected databaseConnectionId and resolve PostgreSQL credentials from keytar - construct the SQLAlchemy catalog URI in memory and pass it to the Python bridge through an environment placeholder - add sql-postgres, psycopg2, SQLAlchemy, PyArrow, and S3FS runtime verification - separate catalog property construction from warehouse and FileIO configuration - support local, server-managed, and cloud warehouse contracts - generate file, S3, Azure, and GCS warehouse URIs from the selected storage connection - support AWS S3, Azure, GCS, MinIO, Cloudflare R2, Backblaze B2, rustfs, and Garage connections - preserve concrete S3-compatible provider identities instead of collapsing them into AWS - retain the inline Add Connection action, refresh the connection list after creation, and auto-select the new connection - reuse Cloud Explorer credential ownership without persisting duplicate cloud secrets - extend Iceberg instance list records with catalog, warehouse, and storage location metadata - guard DuckLake-only connection acquisition when navigating to Iceberg instances --- resources/python/iceberg_bridge.py | 66 +- scripts/generate_local_iceberg_sample.py | 56 ++ .../icebergDatalake.ipcHandlers.ts | 4 + src/main/services/icebergDatalake.service.ts | 402 ++++++-- .../cloudExplorer/ConnectionForm.tsx | 24 +- .../dataLake/DataLakeConnectionSelector.tsx | 234 +++-- .../components/dataLake/DataLakeDashboard.tsx | 282 +++--- .../components/dataLake/DataLakeInstances.tsx | 245 +++-- .../components/dataLake/DataLakeSidebar.tsx | 91 +- .../dataLake/IcebergConnectionWizard.tsx | 947 ++++++++++++++++++ .../dataLake/IcebergInstanceListItem.tsx | 145 +++ src/renderer/components/dataLake/index.ts | 2 + .../controllers/icebergDatalake.controller.ts | 45 + src/renderer/screens/dataLake/index.tsx | 520 +++++++++- src/renderer/services/iceberg.service.ts | 7 +- src/types/iceberg.ts | 67 +- src/types/ipc.ts | 19 +- tests/python/test_iceberg_bridge.py | 65 ++ 18 files changed, 2785 insertions(+), 436 deletions(-) create mode 100644 scripts/generate_local_iceberg_sample.py create mode 100644 src/renderer/components/dataLake/IcebergConnectionWizard.tsx create mode 100644 src/renderer/components/dataLake/IcebergInstanceListItem.tsx create mode 100644 tests/python/test_iceberg_bridge.py diff --git a/resources/python/iceberg_bridge.py b/resources/python/iceberg_bridge.py index b3275032..4835cdb1 100644 --- a/resources/python/iceberg_bridge.py +++ b/resources/python/iceberg_bridge.py @@ -27,9 +27,13 @@ def resolve_env_vars(props: dict) -> dict: def handle_install_check(_cmd: dict) -> dict: - """Check if pyiceberg is installed and return its version.""" + """Check the complete runtime profile required by the implemented catalogs.""" try: import pyiceberg # noqa: PLC0415 + import pyarrow # noqa: F401, PLC0415 + import psycopg2 # noqa: F401, PLC0415 + import s3fs # noqa: F401, PLC0415 + import sqlalchemy # noqa: F401, PLC0415 return {"ok": True, "installed": True, "version": pyiceberg.__version__} except ImportError: return {"ok": True, "installed": False} @@ -155,33 +159,43 @@ def handle_preview_table(cmd: dict) -> dict: def handle_create_metadata_file(cmd: dict) -> dict: """ - Initialize a local file-based Iceberg catalog at the given warehouse path. - Creates the metadata directory and writes a minimal catalog metadata JSON. + Initialize a development-only SQL catalog backed by SQLite. + + The IPC command keeps its historical name for compatibility. It creates a + durable catalog database and local warehouse, then reloads the catalog to + prove the persisted contract works. """ try: - import uuid # noqa: PLC0415 - warehouse_path = cmd["warehouse_path"] - metadata_dir = os.path.join(warehouse_path, "metadata") - os.makedirs(metadata_dir, exist_ok=True) - metadata_path = os.path.join(metadata_dir, "v1.metadata.json") - if not os.path.exists(metadata_path): - catalog_metadata = { - "format-version": 1, - "table-uuid": str(uuid.uuid4()), - "location": warehouse_path, - "last-updated-ms": 0, - "last-column-id": 0, - "schema": {"type": "struct", "fields": []}, - "partition-spec": [], - "properties": {}, - "current-snapshot-id": -1, - "snapshots": [], - "snapshot-log": [], - "metadata-log": [], - } - with open(metadata_path, "w", encoding="utf-8") as f: - json.dump(catalog_metadata, f, indent=2) - return {"ok": True, "metadata_path": metadata_path} + from pathlib import Path # noqa: PLC0415 + from pyiceberg.catalog import load_catalog # noqa: PLC0415 + + catalog_dir = Path(cmd["warehouse_path"]).expanduser().resolve() + catalog_dir.mkdir(parents=True, exist_ok=True) + warehouse_dir = catalog_dir / "warehouse" + warehouse_dir.mkdir(parents=True, exist_ok=True) + catalog_path = catalog_dir / "pyiceberg_catalog.db" + + properties = { + "type": "sql", + "uri": f"sqlite:///{catalog_path.as_posix()}", + "warehouse": warehouse_dir.as_uri(), + } + catalog = load_catalog("local", **properties) + catalog.create_namespace_if_not_exists("default") + catalog.close() + + reloaded = load_catalog("local", **properties) + namespaces = [list(namespace) for namespace in reloaded.list_namespaces()] + tables = [list(identifier) for identifier in reloaded.list_tables("default")] + reloaded.close() + + return { + "ok": True, + "metadata_path": str(catalog_path), + "warehouse_path": str(warehouse_dir), + "namespaces": namespaces, + "tables": tables, + } except Exception as exc: # noqa: BLE001 return {"ok": False, "error": str(exc)} diff --git a/scripts/generate_local_iceberg_sample.py b/scripts/generate_local_iceberg_sample.py new file mode 100644 index 00000000..300a9b19 --- /dev/null +++ b/scripts/generate_local_iceberg_sample.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Generate sample data in a DBT Studio local Iceberg catalog.""" + +import argparse +from pathlib import Path + +import pyarrow as pa +from pyiceberg.catalog import load_catalog + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "catalog_directory", + help="Folder selected in the DBT Studio Local (SQLite catalog) picker", + ) + args = parser.parse_args() + + catalog_directory = Path(args.catalog_directory).expanduser().resolve() + catalog_path = catalog_directory / "pyiceberg_catalog.db" + warehouse_path = catalog_directory / "warehouse" + if not catalog_path.is_file(): + raise SystemExit( + f"Catalog database not found: {catalog_path}. Initialize it in DBT Studio first." + ) + + catalog = load_catalog( + "local", + **{ + "type": "sql", + "uri": f"sqlite:///{catalog_path.as_posix()}", + "warehouse": warehouse_path.as_uri(), + }, + ) + catalog.create_namespace_if_not_exists("default") + sample = pa.table( + { + "id": pa.array([1, 2, 3], type=pa.int64()), + "name": pa.array(["alpha", "beta", "gamma"], type=pa.string()), + } + ) + table = catalog.create_table_if_not_exists( + ("default", "generated_data"), + schema=sample.schema, + ) + table.append(sample) + rows = table.scan().to_arrow().to_pylist() + catalog.close() + + print(f"Catalog: {catalog_path}") + print("Table: default.generated_data") + print(f"Rows: {rows}") + + +if __name__ == "__main__": + main() diff --git a/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts b/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts index 27dbf884..3e1fd1db 100644 --- a/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts +++ b/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts @@ -8,6 +8,10 @@ import { ipcMain } from 'electron'; import { IcebergDatalakeService } from '../services/icebergDatalake.service'; export const registerIcebergDatalakeHandlers = () => { + ipcMain.handle('iceberg:getCapabilities', () => + IcebergDatalakeService.getCapabilities(), + ); + ipcMain.handle('iceberg:list', () => IcebergDatalakeService.listInstances()); ipcMain.handle('iceberg:get', (_e, id: string) => diff --git a/src/main/services/icebergDatalake.service.ts b/src/main/services/icebergDatalake.service.ts index b6603009..2b3491d5 100644 --- a/src/main/services/icebergDatalake.service.ts +++ b/src/main/services/icebergDatalake.service.ts @@ -9,6 +9,7 @@ import { v4 as uuidv4 } from 'uuid'; import * as path from 'path'; +import { pathToFileURL } from 'url'; import { spawn } from 'child_process'; import { app } from 'electron'; @@ -26,10 +27,88 @@ import type { IcebergFieldSpec, IcebergSnapshotInfo, IcebergPreviewResult, + IcebergLocalCatalogResult, + IcebergCapabilities, + IcebergCatalogCapability, } from '../../types/iceberg'; import type { CloudConnection } from '../../types/frontend'; +import type { PostgresConnection } from '../../types/backend'; export class IcebergDatalakeService { + private static readonly cloudProviders = [ + 'aws', + 'azure', + 'gcs', + 'minio', + 'cloudflare-r2', + 'backblaze-b2', + 'rustfs', + 'garage', + ] as const; + + private static readonly catalogCapabilities: IcebergCatalogCapability[] = [ + { + type: 'sqlite', + label: 'SQLite (Local)', + pyicebergType: 'sql', + enabled: true, + requiredFields: ['catalogPath'], + authModes: ['none'], + allowedStorageTypes: ['local', 'cloud'], + }, + { + type: 'sql', + label: 'PostgreSQL / Neon', + pyicebergType: 'sql', + enabled: true, + requiredFields: ['databaseConnectionId'], + authModes: ['none'], + allowedStorageTypes: ['local', 'cloud'], + }, + { + type: 'rest', + label: 'REST Catalog', + pyicebergType: 'rest', + enabled: true, + requiredFields: ['endpoint', 'catalogName'], + authModes: ['none', 'token'], + allowedStorageTypes: ['server-managed'], + }, + { + type: 'polaris', + label: 'Apache Polaris', + pyicebergType: 'rest', + enabled: true, + requiredFields: ['endpoint', 'catalogName'], + authModes: ['none', 'token'], + allowedStorageTypes: ['server-managed'], + }, + ...( + [ + ['hive', 'Hive Metastore', 'hive'], + ['hadoop', 'Hadoop Catalog', 'custom'], + ['glue', 'AWS Glue', 'glue'], + ['nessie', 'Project Nessie', 'rest'], + ] as const + ).map(([type, label, pyicebergType]) => ({ + type, + label, + pyicebergType, + enabled: false, + disabledReason: 'Planned for the next catalog adapter slice.', + requiredFields: [], + authModes: ['none'] as IcebergCatalogCapability['authModes'], + allowedStorageTypes: [], + })), + ]; + + // In-process cache: once we confirm pyiceberg is installed for this app + // session we skip the Python bridge check on subsequent calls. + private static installedCache: { + installed: boolean; + version?: string; + } | null = null; + // ───────────────────────────────────────────── // Private: persistence helpers // ───────────────────────────────────────────── @@ -37,7 +116,11 @@ export class IcebergDatalakeService { private static async readInstances(): Promise { try { const db = await loadDatabaseFile(); - return db.icebergInstances ?? []; + return (db.icebergInstances ?? []).map((instance) => { + const persisted = instance as unknown as { catalogType: string }; + if (persisted.catalogType !== 'file') return instance; + return { ...instance, catalogType: 'sqlite' }; + }); } catch (error) { // eslint-disable-next-line no-console console.error('[IcebergDatalakeService] readInstances error:', error); @@ -63,7 +146,6 @@ export class IcebergDatalakeService { __dirname, '..', '..', - '..', 'resources', 'python', 'iceberg_bridge.py', @@ -136,14 +218,27 @@ export class IcebergDatalakeService { const env: Record = {}; switch (instance.catalogType) { - case 'file': - props.type = 'rest'; - if (instance.catalogPath) props.uri = instance.catalogPath; + case 'sqlite': + props.type = 'sql'; + if (instance.catalogPath) { + props.uri = `sqlite:///${instance.catalogPath}`; + } + if (instance.localPath) { + props.warehouse = `file://${instance.localPath}`; + } + break; + + case 'sql': { + const sqlCatalog = + await IcebergDatalakeService.buildPostgresCatalogUri(instance); + props.type = 'sql'; + props.uri = '__ENV:ICEBERG_SQL_CATALOG_URI'; + env.ICEBERG_SQL_CATALOG_URI = sqlCatalog; break; + } + case 'rest': case 'polaris': - case 'hive': - case 'sql': props.type = 'rest'; if (instance.endpoint) props.uri = instance.endpoint; if (instance.catalogName) props.warehouse = instance.catalogName; @@ -154,9 +249,9 @@ export class IcebergDatalakeService { instance.catalogAccessTokenKey, ); if (token) { - props.token = '__ENV:ICEBERG_ACCESS_TOKEN__'; + props.token = '__ENV:ICEBERG_ACCESS_TOKEN'; // eslint-disable-next-line dot-notation - env['ICEBERG_ACCESS_TOKEN__'] = token; + env['ICEBERG_ACCESS_TOKEN'] = token; } } catch (tokenError) { // eslint-disable-next-line no-console @@ -168,31 +263,29 @@ export class IcebergDatalakeService { } break; - case 'glue': - props.type = 'glue'; - if (instance.catalogName) props['glue.database'] = instance.catalogName; - break; - - case 'dynamodb': - props.type = 'dynamodb'; - if (instance.catalogName) - props['dynamodb.table-name'] = instance.catalogName; - break; + default: + throw new Error(`ICEBERG_CATALOG_NOT_ENABLED: ${instance.catalogType}`); + } - case 'bigquery': - props.type = 'bigquery'; - if (instance.catalogName) props.gcp_project = instance.catalogName; - break; + const warehouse = + await IcebergDatalakeService.buildWarehouseProperties(instance); + return { + props: { ...props, ...warehouse.props }, + env: { ...env, ...warehouse.env }, + }; + } - case 'in-memory': - props.type = 'in-memory'; - break; + private static async buildWarehouseProperties( + instance: IcebergInstanceConfig, + ): Promise<{ props: Record; env: Record }> { + const props: Record = {}; + const env: Record = {}; - default: - break; + if (instance.storageType === 'server-managed') return { props, env }; + if (instance.storageType === 'local' && instance.localPath) { + props.warehouse = `file://${instance.localPath}`; } - // Storage credentials via Cloud Explorer connection if (instance.storageConnectionId) { try { const db = await loadDatabaseFile(); @@ -266,6 +359,19 @@ export class IcebergDatalakeService { env['ICEBERG_GCS_CREDS__'] = gcsCreds; } } + + if (instance.storageBucket) { + const prefix = instance.storagePrefix + ? `/${instance.storagePrefix.replace(/^\/+|\/+$/g, '')}` + : ''; + if (provider === 'azure' && cfg.accountName) { + props.warehouse = `abfs://${instance.storageBucket}@${cfg.accountName}.dfs.core.windows.net${prefix}`; + } else if (provider === 'gcs') { + props.warehouse = `gs://${instance.storageBucket}${prefix}`; + } else if (s3LikeProviders.includes(provider)) { + props.warehouse = `s3://${instance.storageBucket}${prefix}`; + } + } } } catch (connError) { // eslint-disable-next-line no-console @@ -276,12 +382,115 @@ export class IcebergDatalakeService { } } - if (instance.storageBucket) props['s3.bucket'] = instance.storageBucket; - if (instance.storagePrefix) props['s3.prefix'] = instance.storagePrefix; - return { props, env }; } + private static async buildPostgresCatalogUri( + config: Pick, + ): Promise { + if (!config.databaseConnectionId) { + throw new Error('ICEBERG_REQUIRED_FIELD: databaseConnectionId'); + } + const db = await loadDatabaseFile(); + const model = (db.connections ?? []).find( + (item) => item.id === config.databaseConnectionId, + ); + if (!model || model.connection.type !== 'postgres') { + throw new Error('ICEBERG_SQL_CONNECTION_INVALID'); + } + const connection = model.connection as PostgresConnection; + const username = await secureStorage.getCredential( + `db-user-${connection.name}`, + ); + const password = await secureStorage.getCredential( + `db-password-${connection.name}`, + ); + if (!username || !password) { + throw new Error('ICEBERG_SQL_CREDENTIALS_MISSING'); + } + const auth = `${encodeURIComponent(username)}:${encodeURIComponent( + password, + )}`; + const host = connection.host.trim(); + const database = encodeURIComponent(connection.database); + const sslMode = connection.ssl ? '?sslmode=require' : ''; + return `postgresql+psycopg2://${auth}@${host}:${connection.port}/${database}${sslMode}`; + } + + private static getCatalogCapability( + catalogType: IcebergInstanceConfig['catalogType'], + ): IcebergCatalogCapability { + const capability = IcebergDatalakeService.catalogCapabilities.find( + (item) => item.type === catalogType, + ); + if (!capability) { + throw new Error(`ICEBERG_CATALOG_UNSUPPORTED: ${catalogType}`); + } + return capability; + } + + private static validateCatalogWarehousePair( + config: Pick< + IcebergInstanceConfig, + | 'catalogType' + | 'storageType' + | 'catalogPath' + | 'endpoint' + | 'catalogName' + | 'databaseConnectionId' + | 'localPath' + | 'storageConnectionId' + | 'storageBucket' + >, + validateWarehouseFields = true, + ): void { + const capability = IcebergDatalakeService.getCatalogCapability( + config.catalogType, + ); + if (!capability.enabled) { + throw new Error(`ICEBERG_CATALOG_NOT_ENABLED: ${config.catalogType}`); + } + if (!capability.allowedStorageTypes.includes(config.storageType)) { + throw new Error( + `ICEBERG_WAREHOUSE_NOT_ALLOWED: ${config.catalogType}/${config.storageType}`, + ); + } + const missingField = capability.requiredFields.find( + (field) => !config[field]?.trim(), + ); + if (missingField) { + throw new Error(`ICEBERG_REQUIRED_FIELD: ${missingField}`); + } + if ( + validateWarehouseFields && + config.storageType === 'local' && + !config.localPath?.trim() + ) { + throw new Error('ICEBERG_REQUIRED_FIELD: localPath'); + } + if ( + validateWarehouseFields && + config.storageType === 'cloud' && + (!config.storageConnectionId?.trim() || !config.storageBucket?.trim()) + ) { + throw new Error( + 'ICEBERG_REQUIRED_FIELD: storageConnectionId/storageBucket', + ); + } + } + + static getCapabilities(): IcebergCapabilities { + return { + catalogs: IcebergDatalakeService.catalogCapabilities.map((item) => ({ + ...item, + authModes: [...item.authModes], + requiredFields: [...item.requiredFields], + allowedStorageTypes: [...item.allowedStorageTypes], + })), + cloudProviders: [...IcebergDatalakeService.cloudProviders], + }; + } + // ───────────────────────────────────────────── // Public: CRUD // ───────────────────────────────────────────── @@ -296,6 +505,9 @@ export class IcebergDatalakeService { description, catalogType, storageType, + catalogPath, + localPath, + storageBucket, createdAt, updatedAt, }) => ({ @@ -304,6 +516,9 @@ export class IcebergDatalakeService { description, catalogType, storageType, + catalogPath, + localPath, + storageBucket, createdAt, updatedAt, }), @@ -332,6 +547,7 @@ export class IcebergDatalakeService { data: CreateIcebergInstanceDTO, ): Promise { try { + IcebergDatalakeService.validateCatalogWarehousePair(data); const id = uuidv4(); const now = new Date().toISOString(); @@ -378,6 +594,11 @@ export class IcebergDatalakeService { const idx = instances.findIndex((i) => i.id === id); if (idx < 0) throw new Error(`Iceberg instance not found: ${id}`); + IcebergDatalakeService.validateCatalogWarehousePair({ + ...instances[idx], + ...data, + }); + // Handle access token update if (data.accessToken) { const key = @@ -444,44 +665,61 @@ export class IcebergDatalakeService { const props: Record = {}; const env: Record = {}; + const storageType = + params.storageType ?? + (params.catalogType === 'sqlite' ? 'local' : 'server-managed'); + IcebergDatalakeService.validateCatalogWarehousePair( + { + ...params, + storageType, + }, + false, + ); + if (params.endpoint) props.uri = params.endpoint; if (params.catalogName) props.warehouse = params.catalogName; if (params.catalogPath) props.uri = params.catalogPath; switch (params.catalogType) { - case 'file': - props.type = 'rest'; + case 'sqlite': + props.type = 'sql'; + if (params.catalogPath) { + props.uri = `sqlite:///${params.catalogPath}`; + props.warehouse = `file://${path.join( + path.dirname(params.catalogPath), + 'warehouse', + )}`; + } break; - case 'polaris': - case 'hive': case 'sql': + props.type = 'sql'; + props.uri = '__ENV:ICEBERG_SQL_CATALOG_URI'; + env.ICEBERG_SQL_CATALOG_URI = + await IcebergDatalakeService.buildPostgresCatalogUri(params); + props.warehouse = pathToFileURL( + path.join(app.getPath('temp'), 'dbt-studio-iceberg-test'), + ).href; + break; + case 'rest': + case 'polaris': props.type = 'rest'; if (params.accessToken) { - props.token = '__ENV:ICEBERG_ACCESS_TOKEN__'; + props.token = '__ENV:ICEBERG_ACCESS_TOKEN'; // eslint-disable-next-line dot-notation - env['ICEBERG_ACCESS_TOKEN__'] = params.accessToken; + env['ICEBERG_ACCESS_TOKEN'] = params.accessToken; } break; - case 'glue': - props.type = 'glue'; - break; - case 'dynamodb': - props.type = 'dynamodb'; - break; - case 'bigquery': - props.type = 'bigquery'; - break; - case 'in-memory': - props.type = 'in-memory'; - break; default: - props.type = 'rest'; + throw new Error(`ICEBERG_CATALOG_NOT_ENABLED: ${params.catalogType}`); } await IcebergDatalakeService.runBridge( { command: 'test_connection', - catalog_name: params.catalogName ?? 'test', + catalog_name: + params.catalogType === 'sqlite' + ? 'local' + : (params.catalogName ?? 'test'), catalog_properties: props, }, env, @@ -505,13 +743,18 @@ export class IcebergDatalakeService { installed: boolean; version?: string; }> { + // Fast path 1: in-process session cache + if (IcebergDatalakeService.installedCache?.installed) { + return IcebergDatalakeService.installedCache; + } + try { + // Settings record the last successful installation for diagnostics, but + // do not prove the currently selected Python still has every required + // extra. Verify once per app session before trusting it. const settings = await SettingsService.loadSettings(); - if (settings.icebergInstalled) { - return { installed: true, version: settings.icebergVersion }; - } - // Check if already installed + // Check via Python bridge (runs pip only if the runtime profile is incomplete) const checkResult = (await IcebergDatalakeService.runBridge({ command: 'install_check', })) as Record; @@ -523,7 +766,8 @@ export class IcebergDatalakeService { icebergInstalled: true, icebergVersion: version, }); - return { installed: true, version }; + IcebergDatalakeService.installedCache = { installed: true, version }; + return IcebergDatalakeService.installedCache; } // Install pyiceberg with common extras @@ -533,7 +777,10 @@ export class IcebergDatalakeService { '-m', 'pip', 'install', - 'pyiceberg[s3fs,glue,hive,sql-sqlite,sql-postgres,pyarrow]', + // SQLite and PostgreSQL SQL catalogs plus the current FileIO profile. + // --prefer-binary avoids slow source compilation where wheels exist. + 'pyiceberg[s3fs,sql-sqlite,sql-postgres,pyarrow]', + '--prefer-binary', '--quiet', ]); child.on('close', (code: number) => { @@ -556,7 +803,8 @@ export class IcebergDatalakeService { icebergInstalled: true, icebergVersion: version, }); - return { installed: true, version }; + IcebergDatalakeService.installedCache = { installed: true, version }; + return IcebergDatalakeService.installedCache; } return { installed: false }; @@ -585,7 +833,10 @@ export class IcebergDatalakeService { const result = (await IcebergDatalakeService.runBridge( { command: 'list_namespaces', - catalog_name: instance.catalogName ?? id, + catalog_name: + instance.catalogType === 'sqlite' + ? 'local' + : (instance.catalogName ?? id), catalog_properties: props, parent: parent ?? [], }, @@ -607,7 +858,10 @@ export class IcebergDatalakeService { const result = (await IcebergDatalakeService.runBridge( { command: 'list_tables', - catalog_name: instance.catalogName ?? id, + catalog_name: + instance.catalogType === 'sqlite' + ? 'local' + : (instance.catalogName ?? id), catalog_properties: props, namespace, }, @@ -633,7 +887,10 @@ export class IcebergDatalakeService { const result = (await IcebergDatalakeService.runBridge( { command: 'get_schema', - catalog_name: instance.catalogName ?? id, + catalog_name: + instance.catalogType === 'sqlite' + ? 'local' + : (instance.catalogName ?? id), catalog_properties: props, namespace, table, @@ -660,7 +917,10 @@ export class IcebergDatalakeService { const result = (await IcebergDatalakeService.runBridge( { command: 'get_snapshots', - catalog_name: instance.catalogName ?? id, + catalog_name: + instance.catalogType === 'sqlite' + ? 'local' + : (instance.catalogName ?? id), catalog_properties: props, namespace, table, @@ -689,7 +949,10 @@ export class IcebergDatalakeService { const result = (await IcebergDatalakeService.runBridge( { command: 'preview_table', - catalog_name: instance.catalogName ?? id, + catalog_name: + instance.catalogType === 'sqlite' + ? 'local' + : (instance.catalogName ?? id), catalog_properties: props, namespace, table, @@ -714,13 +977,20 @@ export class IcebergDatalakeService { // Public: file helpers // ───────────────────────────────────────────── - static async createMetadataFile(warehousePath: string): Promise { + static async createMetadataFile( + warehousePath: string, + ): Promise { try { const result = (await IcebergDatalakeService.runBridge({ command: 'create_metadata_file', warehouse_path: warehousePath, })) as Record; - return (result.metadata_path as string) ?? ''; + return { + catalogPath: (result.metadata_path as string) ?? '', + warehousePath: (result.warehouse_path as string) ?? '', + namespaces: (result.namespaces as string[][]) ?? [], + tables: (result.tables as string[][]) ?? [], + }; } catch (error) { // eslint-disable-next-line no-console console.error( diff --git a/src/renderer/components/cloudExplorer/ConnectionForm.tsx b/src/renderer/components/cloudExplorer/ConnectionForm.tsx index a5bf1cfe..83fa0b1d 100644 --- a/src/renderer/components/cloudExplorer/ConnectionForm.tsx +++ b/src/renderer/components/cloudExplorer/ConnectionForm.tsx @@ -59,6 +59,9 @@ interface ConnectionFormProps { connectionId?: string; duplicateFrom?: CloudConnection; suggestedName?: string; + initialProvider?: CloudProvider; + onSaved?: (connection: CloudConnection) => void | Promise; + onCancel?: () => void; } interface FormData { @@ -92,6 +95,9 @@ export const ConnectionForm: React.FC = ({ connectionId, duplicateFrom, suggestedName, + initialProvider, + onSaved, + onCancel, }) => { const navigate = useNavigate(); const saveConnection = useSaveConnection(); @@ -125,7 +131,11 @@ export const ConnectionForm: React.FC = ({ const [showPassword, setShowPassword] = useState(false); const [formData, setFormData] = useState({ name: initialValues?.name || suggestedName || '', - provider: initialValues?.provider || duplicateFrom?.provider || 'gcs', + provider: + initialValues?.provider || + duplicateFrom?.provider || + initialProvider || + 'gcs', projectId: '', credentials: '', region: '', @@ -545,7 +555,11 @@ export const ConnectionForm: React.FC = ({ lastUsed: new Date(), }; await saveConnection.mutateAsync(connection); - navigate('/app/cloud-explorer/connections'); + if (onSaved) { + await onSaved(connection); + } else { + navigate('/app/cloud-explorer/connections'); + } } catch (error) { setTestStatus('error'); setErrorMessage( @@ -1582,7 +1596,11 @@ export const ConnectionForm: React.FC = ({ diff --git a/src/renderer/components/dataLake/DataLakeConnectionSelector.tsx b/src/renderer/components/dataLake/DataLakeConnectionSelector.tsx index 7e99463b..dc7cd3b1 100644 --- a/src/renderer/components/dataLake/DataLakeConnectionSelector.tsx +++ b/src/renderer/components/dataLake/DataLakeConnectionSelector.tsx @@ -18,7 +18,6 @@ import { } from '@mui/material'; import { Add, - CloudQueue, Storage as StorageIcon, Close, CheckCircle, @@ -35,34 +34,45 @@ import { import useSecureStorage from '../../hooks/useSecureStorage'; import { CloudConnection, + CloudProvider, S3Config, AzureConfig, GCSConfig, } from '../../../types/frontend'; +import { cloudStorageImages } from '../../../../assets/connectionIcons'; +import { ConnectionForm } from '../cloudExplorer/ConnectionForm'; interface DataLakeConnectionSelectorProps { onSelectExisting: ( connectionId: string, bucket: string, prefix?: string, + provider?: CloudProvider, ) => void; - selectedProvider: 'aws' | 'azure' | 'gcs'; + selectedProvider: CloudProvider | 'all'; initialConnectionId?: string; initialBucket?: string; initialPrefix?: string; } -const getProviderIcon = (provider: string) => { - switch (provider) { - case 'aws': - return ; - case 'azure': - return ; - case 'gcs': - return ; - default: - return ; - } +type InlineProvider = 'aws' | 'azure' | 'gcs'; + +const isInlineProvider = ( + provider: CloudProvider | 'all', +): provider is InlineProvider => + provider === 'aws' || provider === 'azure' || provider === 'gcs'; + +const getProviderIcon = (provider: CloudProvider) => { + const iconSrc = cloudStorageImages[provider]; + return iconSrc ? ( + + ) : ( + + ); }; const getProviderLabel = (provider: string) => { @@ -73,6 +83,18 @@ const getProviderLabel = (provider: string) => { return 'Azure Blob Storage'; case 'gcs': return 'Google Cloud Storage'; + case 'minio': + return 'MinIO'; + case 'cloudflare-r2': + return 'Cloudflare R2'; + case 'backblaze-b2': + return 'Backblaze B2'; + case 'rustfs': + return 'rustfs'; + case 'garage': + return 'Garage'; + case 'all': + return 'Cloud Explorer'; default: return provider.toUpperCase(); } @@ -99,8 +121,12 @@ export const DataLakeConnectionSelector: React.FC< // Filter connections by selected provider const filteredConnections = - connections?.filter((conn: any) => conn.provider === selectedProvider) || - []; + connections?.filter( + (conn: any) => + selectedProvider === 'all' || conn.provider === selectedProvider, + ) || []; + + const supportsInlineCreate = isInlineProvider(selectedProvider); const [selectedConnectionId, setSelectedConnectionId] = useState(''); const [bucket, setBucket] = useState(''); @@ -115,9 +141,17 @@ export const DataLakeConnectionSelector: React.FC< // Update parent when connection or bucket changes React.useEffect(() => { if (selectedConnectionId && bucket) { - onSelectExisting(selectedConnectionId, bucket, prefix || undefined); + const selectedConnection = connections?.find( + (connection: CloudConnection) => connection.id === selectedConnectionId, + ); + onSelectExisting( + selectedConnectionId, + bucket, + prefix || undefined, + selectedConnection?.provider, + ); } - }, [selectedConnectionId, bucket, prefix, onSelectExisting]); + }, [selectedConnectionId, bucket, prefix, connections, onSelectExisting]); // Sync initial values from parent when revisiting the step useEffect(() => { @@ -160,6 +194,8 @@ export const DataLakeConnectionSelector: React.FC< >('idle'); const handleTestConnection = async () => { + if (!isInlineProvider(selectedProvider)) return; + setTestError(null); setTestStatus('testing'); @@ -224,6 +260,8 @@ export const DataLakeConnectionSelector: React.FC< }; const handleSaveConnection = async () => { + if (!isInlineProvider(selectedProvider)) return; + setTestError(null); // Validate required fields @@ -628,78 +666,100 @@ export const DataLakeConnectionSelector: React.FC< setIsModalOpen(false)} - maxWidth="sm" + maxWidth={supportsInlineCreate ? 'sm' : 'lg'} fullWidth > - - Create New {getProviderLabel(selectedProvider)} Connection - setIsModalOpen(false)} - sx={{ position: 'absolute', right: 8, top: 8 }} - > - - - - - {renderNewConnectionForm()} - - {testStatus === 'error' && ( - }> - Connection Error - - {testError || - 'Failed to connect to storage provider. Please check your credentials.'} - - - )} - - {testStatus === 'success' && ( - }> - Connection Successful - - Successfully connected to storage provider. - - - )} - - - - - - + {!supportsInlineCreate ? ( + + setIsModalOpen(false)} + onSaved={async (connection) => { + await refetch(); + setSelectedConnectionId(connection.id); + setBucket(''); + setPrefix(''); + setIsModalOpen(false); + }} + /> + + ) : ( + <> + + Create New {getProviderLabel(selectedProvider)} Connection + setIsModalOpen(false)} + sx={{ position: 'absolute', right: 8, top: 8 }} + > + + + + + {renderNewConnectionForm()} + + {testStatus === 'error' && ( + }> + Connection Error + + {testError || + 'Failed to connect to storage provider. Please check your credentials.'} + + + )} + + {testStatus === 'success' && ( + }> + + Connection Successful + + + Successfully connected to storage provider. + + + )} + + + + + + + + )} ); diff --git a/src/renderer/components/dataLake/DataLakeDashboard.tsx b/src/renderer/components/dataLake/DataLakeDashboard.tsx index c19ea712..512819db 100644 --- a/src/renderer/components/dataLake/DataLakeDashboard.tsx +++ b/src/renderer/components/dataLake/DataLakeDashboard.tsx @@ -17,6 +17,7 @@ import { Dashboard, Add, Folder, + LocalFireDepartment, } from '@mui/icons-material'; import { cloudStorageImages, @@ -24,6 +25,7 @@ import { } from '../../../../assets/connectionIcons'; import { icons } from '../../../../assets/icons'; import { DataLakeSVG } from '../sidebar/icons'; +import type { IcebergInstanceListItem } from '../../../types/iceberg'; interface DuckLakeInstance { id: string; @@ -37,6 +39,24 @@ interface DuckLakeInstance { updatedAt: string; } +type RecentDataLakeItem = + | { + id: string; + name: string; + lakeType: 'duck-lake'; + catalogLabel: string; + dataPath: string; + updatedAt: string; + } + | { + id: string; + name: string; + lakeType: 'iceberg'; + catalogLabel: string; + dataPath: string; + updatedAt: string; + }; + const getStorageIconForInstance = (dataPath: string) => { if (dataPath.startsWith('s3://')) { return ( @@ -71,22 +91,19 @@ const getStorageIconForInstance = (dataPath: string) => { return ; }; -interface DuckLakeDashboardProps { - instances?: DuckLakeInstance[]; +interface DataLakeDashboardProps { + duckLakeInstances?: DuckLakeInstance[]; + icebergInstances?: IcebergInstanceListItem[]; } -export const DataLakeDashboard: React.FC = ({ - instances = [], +export const DataLakeDashboard: React.FC = ({ + duckLakeInstances = [], + icebergInstances = [], }) => { const navigate = useNavigate(); - // Calculate statistics const stats = useMemo(() => { - const activeInstances = instances.filter( - (i) => i.status === 'active', - ).length; - const totalInstances = instances.length; - const catalogTypes = instances.reduce( + const duckLakeCatalogTypes = duckLakeInstances.reduce( (acc, instance) => { acc[instance.catalog.type] = (acc[instance.catalog.type] || 0) + 1; return acc; @@ -94,16 +111,96 @@ export const DataLakeDashboard: React.FC = ({ {} as Record, ); + const icebergCatalogTypes = icebergInstances.reduce( + (acc, instance) => { + const key = `iceberg-${instance.catalogType}`; + acc[key] = (acc[key] || 0) + 1; + return acc; + }, + {} as Record, + ); + + const catalogTypes = { ...duckLakeCatalogTypes, ...icebergCatalogTypes }; + + const recentItems: RecentDataLakeItem[] = [ + ...duckLakeInstances.map((instance) => ({ + id: instance.id, + name: instance.name, + lakeType: 'duck-lake' as const, + catalogLabel: instance.catalog.type.toUpperCase(), + dataPath: instance.dataPath, + updatedAt: instance.updatedAt, + })), + ...icebergInstances.map((instance) => ({ + id: instance.id, + name: instance.name, + lakeType: 'iceberg' as const, + catalogLabel: instance.catalogType.toUpperCase(), + dataPath: + instance.localPath || + instance.catalogPath || + instance.storageBucket || + instance.storageType, + updatedAt: instance.updatedAt, + })), + ].sort( + (a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), + ); + return { - activeInstances, - totalInstances, + duckLakeCount: duckLakeInstances.length, + icebergCount: icebergInstances.length, + totalInstances: duckLakeInstances.length + icebergInstances.length, catalogTypes, + recentItems, }; - }, [instances]); + }, [duckLakeInstances, icebergInstances]); + + const getCatalogIcon = (type: string) => { + if (type.startsWith('iceberg-')) { + return ( + + ); + } + + let iconSrc; + switch (type.toLowerCase()) { + case 'duckdb': + iconSrc = databaseIcons.duckdb; + break; + case 'sqlite': + iconSrc = databaseIcons.sqlite; + break; + case 'postgres': + case 'postgresql': + iconSrc = databaseIcons.postgresql; + break; + default: + iconSrc = databaseIcons.duckdb; + } + return ( + + ); + }; + + const getCatalogLabel = (type: string, count: number) => { + if (type.startsWith('iceberg-')) { + const catalogType = type.replace('iceberg-', '').toUpperCase(); + return `${count} Iceberg ${catalogType} ${count === 1 ? 'catalog' : 'catalogs'}`; + } + return `${count} ${type.toUpperCase()} ${count === 1 ? 'catalog' : 'catalogs'}`; + }; return ( - {/* Header with title and manage datalakes button */} = ({ mb: 3, }} > - + DataLake Dashboard @@ -140,7 +231,6 @@ export const DataLakeDashboard: React.FC = ({ gap: 3, }} > - {/* Statistics Cards */} = ({ Total DataLakes - - - + } sx={{ pb: 1 }} @@ -193,7 +281,7 @@ export const DataLakeDashboard: React.FC = ({ - {instances.length} + {stats.duckLakeCount} = ({ component="img" src={icons.apacheIcebergLake} alt="Apache Iceberg" - sx={{ width: 16, height: 16, opacity: 0.4 }} + sx={{ + width: 16, + height: 16, + opacity: stats.icebergCount > 0 ? 1 : 0.4, + }} /> 0 ? 1 : 0.6 }} > Apache Iceberg 0 ? 500 : undefined, + opacity: stats.icebergCount > 0 ? 1 : 0.6, + }} > - 0 + {stats.icebergCount} = ({ sx={{ pb: 1 }} /> - + 0 @@ -359,11 +449,7 @@ export const DataLakeDashboard: React.FC = ({ sx={{ pb: 1 }} /> - + 0 @@ -415,51 +501,19 @@ export const DataLakeDashboard: React.FC = ({ gap: 0.5, }} > - {Object.entries(stats.catalogTypes).map(([type, count]) => { - const getCatalogIcon = () => { - let iconSrc; - switch (type.toLowerCase()) { - case 'duckdb': - iconSrc = databaseIcons.duckdb; - break; - case 'sqlite': - iconSrc = databaseIcons.sqlite; - break; - case 'postgres': - case 'postgresql': - iconSrc = databaseIcons.postgresql; - break; - default: - iconSrc = databaseIcons.duckdb; - } - return ( - - ); - }; - return ( - - {getCatalogIcon()} - - {count} {type.toUpperCase()}{' '} - {count === 1 ? 'catalog' : 'catalogs'} - - - ); - })} + {Object.entries(stats.catalogTypes).map(([type, count]) => ( + + {getCatalogIcon(type)} + + {getCatalogLabel(type, count)} + + + ))} - {/* Recent Activity */} = ({ mt: 3, }} > - {/* Recent DataLakes */} = ({ Recently updated DataLakes - - - + } /> @@ -508,15 +559,15 @@ export const DataLakeDashboard: React.FC = ({ sx={{ display: 'flex', flexDirection: 'column', height: '100%' }} > - {instances.length === 0 ? ( + {stats.recentItems.length === 0 ? ( No DataLakes created yet ) : ( - {instances.slice(0, 5).map((instance) => ( + {stats.recentItems.slice(0, 5).map((instance) => ( = ({ }} onClick={() => navigate( - `/app/data-lake/duck-lake/instances/${instance.id}`, + instance.lakeType === 'iceberg' + ? `/app/data-lake/iceberg/instances/${instance.id}` + : `/app/data-lake/duck-lake/instances/${instance.id}`, ) } > - {getStorageIconForInstance(instance.dataPath)} + {instance.lakeType === 'iceberg' ? ( + + ) : ( + getStorageIconForInstance(instance.dataPath) + )} {instance.name} - {instance.catalog.type.toUpperCase()} catalog + {instance.catalogLabel} catalog - + {instance.lakeType === 'iceberg' ? ( + + ) : ( + + )} {moment(instance.updatedAt).fromNow()} @@ -577,7 +644,6 @@ export const DataLakeDashboard: React.FC = ({ - {/* Recent Queries */} = ({ Coming Soon Query history tracking is not yet implemented @@ -645,8 +705,7 @@ export const DataLakeDashboard: React.FC = ({ - {/* Welcome Card for New Users */} - {instances.length === 0 && ( + {stats.totalInstances === 0 && ( = ({ /> - DataLake allows you to create and manage DataLakes with various - catalog backends including DuckDB, SQLite, and PostgreSQL. Start - by creating your first DataLake. + DataLake allows you to create and manage DuckLake and Apache + Iceberg instances. Start by creating your first DataLake. + )} refetch()} + onClick={handleRefresh} disabled={isLoading} title="Refresh" > @@ -140,15 +252,13 @@ export const DataLakeInstances: React.FC = ({ - {/* Loading State */} {isLoading && ( )} - {/* Empty State */} - {!isLoading && instances.length === 0 && ( + {!isLoading && rows.length === 0 && ( @@ -163,8 +273,7 @@ export const DataLakeInstances: React.FC = ({ )} - {/* Instances Table */} - {!isLoading && instances.length > 0 && ( + {!isLoading && rows.length > 0 && ( @@ -172,89 +281,92 @@ export const DataLakeInstances: React.FC = ({ Name DataLake Type Catalog Type - Data Path + Data Path / Storage Created Last Updated Actions - {instances.map((instance) => ( + {rows.map((row) => ( handleInstanceClick(instance.id)} + onClick={() => handleInstanceClick(row)} > - - {instance.name} + + {row.name} - {instance.description && ( + {row.description && ( - {instance.description} + {row.description} )} - + {row.lakeType === 'duck-lake' ? ( + + ) : ( + } + label="Apache Iceberg" + size="small" + color="primary" + variant="outlined" + /> + )} - + - - {instance.dataPath} + + {row.dataPath} - {moment(instance.createdAt).format('MMM D, YYYY')} + {moment(row.createdAt).format('MMM D, YYYY')} - {moment(instance.createdAt).fromNow()} + {moment(row.createdAt).fromNow()} - {moment(instance.updatedAt).format('MMM D, YYYY')} + {moment(row.updatedAt).format('MMM D, YYYY')} - {moment(instance.updatedAt).fromNow()} + {moment(row.updatedAt).fromNow()} e.stopPropagation()}> handleEdit(instance.id)} + onClick={() => handleEdit(row)} title="Edit" > handleDelete(instance.id)} - disabled={deleteMutation.isLoading} + onClick={() => handleDelete(row)} + disabled={ + deleteDuckLakeMutation.isLoading || + deleteIcebergMutation.isLoading + } title="Delete" color="error" > @@ -269,7 +381,6 @@ export const DataLakeInstances: React.FC = ({ )} - {/* Delete Confirmation Modal */} = ({ instances = [], + icebergInstances = [], }) => { const theme = useTheme(); const location = useLocation(); const navigate = useNavigate(); - // Extract instanceId from current path + // Extract lake type + instanceId from current path const pathSegments = location.pathname.split('/'); - const instanceId = pathSegments.includes('instances') - ? pathSegments[pathSegments.indexOf('instances') + 1] - : null; + const instancesIndex = pathSegments.indexOf('instances'); + const instanceId = + instancesIndex >= 0 ? pathSegments[instancesIndex + 1] : null; + const lakeType = + instancesIndex > 0 ? pathSegments[instancesIndex - 1] : null; - const selectedInstance = instances.find( - (instance) => instance.id === instanceId, + const selectedDuckLakeInstance = instances.find( + (instance) => lakeType === 'duck-lake' && instance.id === instanceId, + ); + const selectedIcebergInstance = icebergInstances.find( + (instance) => lakeType === 'iceberg' && instance.id === instanceId, ); return ( @@ -165,7 +173,7 @@ export const DataLakeSidebar: React.FC = ({ ))} - {/* Instances List */} + {/* DuckLake Instances */} {instances.length > 0 && ( = ({ letterSpacing: '0.5px', }} > - DataLakes + DuckLake = ({ mb: 0, width: '270px', backgroundColor: - selectedInstance?.id === instance.id + selectedDuckLakeInstance?.id === instance.id ? theme.palette.divider : 'transparent', }} @@ -227,6 +235,69 @@ export const DataLakeSidebar: React.FC = ({ )} + + {/* Iceberg Instances */} + {icebergInstances.length > 0 && ( + + + Apache Iceberg + + + {icebergInstances.map((instance) => ( + + + + + + + + + ))} + + + )} diff --git a/src/renderer/components/dataLake/IcebergConnectionWizard.tsx b/src/renderer/components/dataLake/IcebergConnectionWizard.tsx new file mode 100644 index 00000000..dd06ff31 --- /dev/null +++ b/src/renderer/components/dataLake/IcebergConnectionWizard.tsx @@ -0,0 +1,947 @@ +/** + * Iceberg Connection Wizard + * 4-step wizard for creating/editing Apache Iceberg instances. + * Pattern: mirrors DataLakeConnectionWizard structure (DuckLake). + */ + +import React, { useState, useEffect } from 'react'; +import { + Box, + Button, + TextField, + Typography, + Stepper, + Step, + StepLabel, + FormControl, + InputLabel, + Select, + MenuItem, + Alert, + Chip, + CircularProgress, + Paper, + Divider, + IconButton, + Tooltip, + List, + ListItem, + ListItemText, +} from '@mui/material'; +import { + ArrowForward, + ArrowBack, + CheckCircle, + FolderOpen, + LocalFireDepartment, + Speed, + Visibility, + VisibilityOff, +} from '@mui/icons-material'; +import type { + IcebergCatalogType, + IcebergCloudProvider, + IcebergInstanceConfig, + IcebergStorageType, +} from '../../../types/iceberg'; +import { useFilePicker, useGetConnections } from '../../controllers'; +import { + useCreateIcebergMetadataFile, + useIcebergCapabilities, + useTestIcebergCatalog, +} from '../../controllers/icebergDatalake.controller'; +import { DataLakeConnectionSelector } from './DataLakeConnectionSelector'; + +// ─── Wizard Data ───────────────────────────────────────────────────────────── + +export interface IcebergWizardData { + basics: { + name: string; + description?: string; + }; + catalog: { + catalogType: IcebergCatalogType; + catalogPath?: string; + endpoint?: string; + catalogName?: string; + databaseConnectionId?: string; + accessToken?: string; + polarisConnectionId?: string; + polarisBucket?: string; + polarisPrefix?: string; + }; + storage: { + storageType: IcebergStorageType; + localPath?: string; + cloudProvider?: IcebergCloudProvider; + connectionId?: string; + bucket?: string; + prefix?: string; + }; +} + +export interface IcebergConnectionWizardProps { + onComplete: (data: IcebergWizardData) => void | Promise; + onCancel: () => void; + isLoading?: boolean; + /** Pre-fill the form from an existing instance (edit mode) */ + initialData?: IcebergInstanceConfig; + mode?: 'create' | 'edit'; +} + +// ─── Step labels ───────────────────────────────────────────────────────────── + +const STEPS = ['Basics', 'Catalog', 'Storage', 'Review']; + +// ─── Default empty state ───────────────────────────────────────────────────── + +const emptyData = (): IcebergWizardData => ({ + basics: { name: '', description: '' }, + catalog: { catalogType: 'sqlite' }, + storage: { storageType: 'local' }, +}); + +function buildInitialData(initial?: IcebergInstanceConfig): IcebergWizardData { + if (!initial) return emptyData(); + return { + basics: { + name: initial.name, + description: initial.description ?? '', + }, + catalog: { + catalogType: initial.catalogType, + catalogPath: initial.catalogPath, + endpoint: initial.endpoint, + catalogName: initial.catalogName, + databaseConnectionId: initial.databaseConnectionId, + // Token is masked — never pre-filled; placeholder shown instead + accessToken: undefined, + polarisConnectionId: initial.catalogConnectionId, + polarisBucket: initial.catalogBucket, + polarisPrefix: initial.catalogPrefix, + }, + storage: { + storageType: + initial.catalogType === 'rest' || initial.catalogType === 'polaris' + ? 'server-managed' + : initial.storageType, + localPath: initial.localPath, + cloudProvider: initial.cloudProvider, + connectionId: initial.storageConnectionId, + bucket: initial.storageBucket, + prefix: initial.storagePrefix, + }, + }; +} + +// ─── Validation helpers ─────────────────────────────────────────────────────── + +function validateStep(step: number, data: IcebergWizardData): string | null { + if (step === 0) { + if (!data.basics.name.trim()) return 'Instance name is required.'; + if (data.basics.name.trim().length > 80) + return 'Instance name must be 80 characters or fewer.'; + } + if (step === 1) { + if (data.catalog.catalogType === 'sqlite' && !data.catalog.catalogPath) { + return 'Catalog database path is required for SQLite.'; + } + if ( + data.catalog.catalogType === 'sql' && + !data.catalog.databaseConnectionId + ) { + return 'A PostgreSQL or Neon connection is required.'; + } + if ( + data.catalog.catalogType === 'rest' || + data.catalog.catalogType === 'polaris' + ) { + if (!data.catalog.endpoint) return 'REST endpoint is required.'; + if (!data.catalog.catalogName) + return 'Catalog name / warehouse is required.'; + } + } + if (step === 2) { + if ( + data.catalog.catalogType === 'rest' || + data.catalog.catalogType === 'polaris' + ) { + // REST catalogs manage warehouse storage server-side; vended creds optional + return null; + } + if (data.storage.storageType === 'local' && !data.storage.localPath) { + return 'Local storage path is required.'; + } + if (data.storage.storageType === 'cloud') { + if (!data.storage.connectionId) + return 'A cloud connection must be selected.'; + if (!data.storage.bucket) return 'Bucket name is required.'; + } + } + return null; +} + +// ─── Main Component ─────────────────────────────────────────────────────────── + +export const IcebergConnectionWizard: React.FC< + IcebergConnectionWizardProps +> = ({ + onComplete, + onCancel, + isLoading = false, + initialData, + mode = 'create', +}) => { + const [activeStep, setActiveStep] = useState(0); + const [data, setData] = useState(() => + buildInitialData(initialData), + ); + const [stepError, setStepError] = useState(null); + const [showToken, setShowToken] = useState(false); + const [catalogTestResult, setCatalogTestResult] = useState<{ + success: boolean; + message: string; + } | null>(null); + + useEffect(() => { + if (initialData) { + setData(buildInitialData(initialData)); + setActiveStep(0); + setStepError(null); + setCatalogTestResult(null); + } + }, [initialData]); + + // Catalog test + const testCatalogMutation = useTestIcebergCatalog(); + const createLocalCatalogMutation = useCreateIcebergMetadataFile(); + const capabilitiesQuery = useIcebergCapabilities(); + const catalogCapabilities = capabilitiesQuery.data?.catalogs ?? []; + const databaseConnectionsQuery = useGetConnections(); + const postgresConnections = (databaseConnectionsQuery.data ?? []).filter( + (connection) => connection.connection.type === 'postgres', + ); + + const { mutate: getFiles } = useFilePicker(); + + const pickFolder = (setter: (path: string) => void) => { + getFiles( + { properties: ['openDirectory'] }, + { + onSuccess: (filePaths) => { + if (filePaths && filePaths.length > 0) { + setter(filePaths[0]); + } + }, + }, + ); + }; + + // ── Step navigation ───────────────────────────────────────────────────── + + const handleNext = () => { + const err = validateStep(activeStep, data); + if (err) { + setStepError(err); + return; + } + setStepError(null); + setCatalogTestResult(null); + setActiveStep((s) => s + 1); + }; + + const handleBack = () => { + setStepError(null); + setCatalogTestResult(null); + setActiveStep((s) => s - 1); + }; + + const handleFinish = async () => { + const err = validateStep(3, data); + if (err) { + setStepError(err); + return; + } + setStepError(null); + await onComplete(data); + }; + + // ── Partial update helpers ────────────────────────────────────────────── + + const patchBasics = (patch: Partial) => + setData((d) => ({ ...d, basics: { ...d.basics, ...patch } })); + + const patchCatalog = (patch: Partial) => + setData((d) => ({ ...d, catalog: { ...d.catalog, ...patch } })); + + const patchStorage = (patch: Partial) => + setData((d) => ({ ...d, storage: { ...d.storage, ...patch } })); + + const initializeLocalCatalog = (catalogDirectory: string) => { + setStepError(null); + createLocalCatalogMutation.mutate(catalogDirectory, { + onSuccess: (result) => { + patchCatalog({ catalogPath: result.catalogPath }); + patchStorage({ + storageType: 'local', + localPath: result.warehousePath, + cloudProvider: undefined, + connectionId: undefined, + bucket: undefined, + prefix: undefined, + }); + setCatalogTestResult({ + success: true, + message: `Local catalog initialized and reloaded (${result.namespaces.length} namespace).`, + }); + }, + onError: (error: unknown) => { + setStepError( + error instanceof Error + ? error.message + : 'Failed to initialize the local catalog.', + ); + }, + }); + }; + + // ── Test catalog ──────────────────────────────────────────────────────── + + const handleTestCatalog = async () => { + setCatalogTestResult(null); + try { + const result = await testCatalogMutation.mutateAsync({ + catalogType: data.catalog.catalogType, + catalogPath: data.catalog.catalogPath, + endpoint: data.catalog.endpoint, + catalogName: data.catalog.catalogName, + connectionId: data.catalog.polarisConnectionId, + accessToken: data.catalog.accessToken, + databaseConnectionId: data.catalog.databaseConnectionId, + storageType: data.storage.storageType, + }); + setCatalogTestResult({ + success: result.success, + message: result.success + ? 'Catalog connection successful.' + : (result.error ?? 'Catalog test failed.'), + }); + } catch (err: any) { + // eslint-disable-next-line no-console + console.error(err); + setCatalogTestResult({ + success: false, + message: err?.message ?? 'Catalog test failed.', + }); + } + }; + + // ── Step content renderers ────────────────────────────────────────────── + + const renderBasicsStep = () => ( + + + Instance Details + + patchBasics({ name: e.target.value })} + required + fullWidth + inputProps={{ maxLength: 80 }} + helperText="A unique name to identify this Iceberg instance (max 80 chars)" + /> + patchBasics({ description: e.target.value })} + fullWidth + multiline + rows={2} + helperText="Optional human-readable description" + /> + + ); + + const renderCatalogStep = () => ( + + + Catalog Configuration + + + + Catalog Type + + + + {capabilitiesQuery.isLoading && ( + Loading catalog capabilities… + )} + {capabilitiesQuery.isError && ( + + Catalog capabilities could not be loaded. Reopen the wizard and try + again. + + )} + + {data.catalog.catalogType === 'sqlite' && ( + + patchCatalog({ catalogPath: e.target.value })} + fullWidth + required + placeholder="/data/my-catalog/pyiceberg_catalog.db" + helperText="Choose a folder to initialize a SQLite catalog and local warehouse" + /> + + pickFolder(initializeLocalCatalog)} + disabled={createLocalCatalogMutation.isLoading} + > + {createLocalCatalogMutation.isLoading ? ( + + ) : ( + + )} + + + + )} + + {data.catalog.catalogType === 'sql' && ( + + PostgreSQL / Neon Connection + + {postgresConnections.length === 0 && ( + + Create and test a PostgreSQL connection in Connections first. Neon + uses the same PostgreSQL connection type with SSL enabled. + + )} + + )} + + {(data.catalog.catalogType === 'rest' || + data.catalog.catalogType === 'polaris') && ( + <> + patchCatalog({ endpoint: e.target.value })} + fullWidth + required + helperText="The REST catalog server URL" + /> + patchCatalog({ catalogName: e.target.value })} + fullWidth + required + helperText="The catalog name or warehouse identifier" + /> + + patchCatalog({ accessToken: e.target.value })} + fullWidth + helperText={ + mode === 'edit' && initialData?.catalogAccessTokenKey + ? 'Leave blank to keep the existing token, or enter a new one to replace it.' + : 'Optional: OAuth2 access token for this catalog' + } + InputProps={{ + endAdornment: ( + setShowToken((v) => !v)} + > + {showToken ? : } + + ), + }} + /> + + + )} + + {/* Test button */} + + + {catalogTestResult && ( + : undefined} + > + {catalogTestResult.message} + + )} + + + ); + + const renderStorageStep = () => { + const isRestCatalog = + data.catalog.catalogType === 'rest' || + data.catalog.catalogType === 'polaris'; + + if (isRestCatalog) { + return ( + + + Storage Configuration + + + REST catalogs manage table storage on the server. Configure a cloud + connection below only if your catalog uses{' '} + vended credentials to delegate access to object + storage. + + + Vended credentials (optional) + + + Select a Cloud Explorer connection and bucket the catalog can use + when delegating storage access. Leave blank if credentials are + handled another way. + + + patchCatalog({ + polarisConnectionId: connectionId, + polarisBucket: bucket, + polarisPrefix: prefix, + }) + } + initialConnectionId={data.catalog.polarisConnectionId} + initialBucket={data.catalog.polarisBucket} + initialPrefix={data.catalog.polarisPrefix} + /> + + ); + } + + return ( + + + Storage Configuration + + + + Storage Type + + + + {data.storage.storageType === 'local' && ( + + patchStorage({ localPath: e.target.value })} + fullWidth + required + helperText="Local directory where Iceberg data files will be stored" + /> + + + pickFolder((p) => patchStorage({ localPath: p })) + } + > + + + + + )} + + {data.storage.storageType === 'cloud' && ( + + patchStorage({ + connectionId, + bucket, + prefix, + cloudProvider: provider, + }) + } + initialConnectionId={data.storage.connectionId} + initialBucket={data.storage.bucket} + initialPrefix={data.storage.prefix} + /> + )} + + ); + }; + + const renderReviewStep = () => { + const hasToken = + !!data.catalog.accessToken || + (mode === 'edit' && !!initialData?.catalogAccessTokenKey); + return ( + + + Review & Confirm + + + + + + + {data.basics.description && ( + + + + )} + + + + } + /> + + {data.catalog.catalogType === 'sqlite' && ( + + + + )} + {data.catalog.catalogType === 'sql' && ( + + + connection.id === data.catalog.databaseConnectionId, + )?.connection.name ?? data.catalog.databaseConnectionId + } + /> + + )} + {(data.catalog.catalogType === 'rest' || + data.catalog.catalogType === 'polaris') && ( + <> + + + + + + + + + + + )} + + {data.catalog.catalogType === 'rest' || + data.catalog.catalogType === 'polaris' ? ( + <> + + + + {data.catalog.polarisConnectionId && ( + <> + + + + {data.catalog.polarisBucket && ( + + + + )} + {data.catalog.polarisPrefix && ( + + + + )} + + )} + + ) : ( + <> + + + } + /> + + {data.storage.storageType === 'local' && ( + + + + )} + {data.storage.storageType === 'cloud' && ( + <> + + + + + + + {data.storage.prefix && ( + + + + )} + + )} + + )} + + + }> + {mode === 'create' + ? 'Clicking "Create Instance" will save these settings and register the Iceberg catalog. No data files will be modified.' + : 'Clicking "Save Changes" will update the instance configuration.'} + + + ); + }; + + const renderStep = () => { + switch (activeStep) { + case 0: + return renderBasicsStep(); + case 1: + return renderCatalogStep(); + case 2: + return renderStorageStep(); + case 3: + return renderReviewStep(); + default: + return null; + } + }; + + const getFinishLabel = () => { + if (isLoading) return 'Saving\u2026'; + if (mode === 'edit') return 'Save Changes'; + return 'Create Instance'; + }; + const finishLabel = getFinishLabel(); + + const isLastStep = activeStep === STEPS.length - 1; + + const getEndIcon = () => { + if (isLoading) return ; + if (isLastStep) return ; + return ; + }; + + return ( + + {/* Header */} + + + + {mode === 'edit' + ? 'Edit Iceberg Instance' + : 'New Apache Iceberg Instance'} + + + + {/* Stepper */} + + {STEPS.map((label) => ( + + {label} + + ))} + + + {/* Step content */} + {renderStep()} + + {/* Step error */} + {stepError && ( + setStepError(null)}> + {stepError} + + )} + + {/* Navigation */} + + + + + + ); +}; diff --git a/src/renderer/components/dataLake/IcebergInstanceListItem.tsx b/src/renderer/components/dataLake/IcebergInstanceListItem.tsx new file mode 100644 index 00000000..e4a2ac6a --- /dev/null +++ b/src/renderer/components/dataLake/IcebergInstanceListItem.tsx @@ -0,0 +1,145 @@ +/** + * IcebergInstanceListItem + * Renders one Iceberg instance row in the DataLake screen instance list. + * Pattern: matches DuckLake instance list item presentation. + */ + +import React from 'react'; +import { + Box, + Typography, + Chip, + IconButton, + Tooltip, + useTheme, +} from '@mui/material'; +import { + LocalFireDepartment, + Edit, + Delete, + Cloud, + Storage as StorageIcon, +} from '@mui/icons-material'; +import type { IcebergInstanceListItem as IcebergInstance } from '../../../types/iceberg'; + +interface IcebergInstanceListItemProps { + instance: IcebergInstance; + isSelected: boolean; + onSelect: (id: string) => void; + onEdit: (id: string) => void; + onDelete: (id: string) => void; +} + +const catalogTypeLabel: Record = { + sqlite: 'SQLite', + rest: 'REST', + polaris: 'Polaris', + glue: 'Glue', + hive: 'Hive', + hadoop: 'Hadoop', + sql: 'SQL', + nessie: 'Nessie', +}; + +export const IcebergInstanceListItem: React.FC< + IcebergInstanceListItemProps +> = ({ instance, isSelected, onSelect, onEdit, onDelete }) => { + const theme = useTheme(); + + return ( + onSelect(instance.id)} + sx={{ + display: 'flex', + alignItems: 'center', + gap: 1.5, + px: 1.5, + py: 1, + borderRadius: 1, + cursor: 'pointer', + borderLeft: isSelected + ? `3px solid ${theme.palette.primary.main}` + : '3px solid transparent', + backgroundColor: isSelected + ? theme.palette.action.selected + : 'transparent', + '&:hover': { + backgroundColor: theme.palette.action.hover, + }, + transition: 'background-color 0.15s ease, border-left-color 0.15s ease', + }} + > + {/* Icon */} + + + {/* Name + chips */} + + + {instance.name} + + + + + ) : ( + + ) + } + label={instance.storageType} + size="small" + variant="outlined" + sx={{ height: 16, fontSize: '0.65rem' }} + /> + + + + {/* Actions */} + e.stopPropagation()} + > + + onEdit(instance.id)} + sx={{ p: 0.5 }} + > + + + + + onDelete(instance.id)} + sx={{ p: 0.5, color: theme.palette.error.main }} + > + + + + + + ); +}; diff --git a/src/renderer/components/dataLake/index.ts b/src/renderer/components/dataLake/index.ts index ed6003cc..0b5986aa 100644 --- a/src/renderer/components/dataLake/index.ts +++ b/src/renderer/components/dataLake/index.ts @@ -8,3 +8,5 @@ export * from './DataLakeInstanceDetails'; export * from './DataLakeInstanceEditForm'; export * from './DataLakeTableImportWizard'; export * from './DataLakeTableDetails'; +export * from './IcebergConnectionWizard'; +export * from './IcebergInstanceListItem'; diff --git a/src/renderer/controllers/icebergDatalake.controller.ts b/src/renderer/controllers/icebergDatalake.controller.ts index a76a5e9c..93c4fc06 100644 --- a/src/renderer/controllers/icebergDatalake.controller.ts +++ b/src/renderer/controllers/icebergDatalake.controller.ts @@ -4,6 +4,7 @@ * Follows the React Query v3 pattern used throughout the app. */ +import React from 'react'; import { useMutation, useQuery, useQueryClient } from 'react-query'; import type { CreateIcebergInstanceDTO, @@ -18,6 +19,11 @@ import * as icebergService from '../services/iceberg.service'; export const useListIcebergInstances = () => useQuery(['iceberg', 'list'], icebergService.listIcebergInstances); +export const useIcebergCapabilities = () => + useQuery(['iceberg', 'capabilities'], icebergService.getIcebergCapabilities, { + staleTime: Infinity, + }); + export const useGetIcebergInstance = (id: string) => useQuery( ['iceberg', 'instance', id], @@ -102,6 +108,11 @@ export const useTestIcebergCatalog = () => icebergService.testIcebergCatalog(params), ); +export const useCreateIcebergMetadataFile = () => + useMutation((warehousePath: string) => + icebergService.createIcebergMetadataFile(warehousePath), + ); + export const usePreviewIcebergTable = () => useMutation( ({ @@ -128,3 +139,37 @@ export const usePreviewIcebergTable = () => export const useEnsureIcebergInstalled = () => useMutation(() => icebergService.ensureIcebergInstalled()); + +/** + * FE-05 — Install gate hook. + * Wraps useMutation side-effect in a dedicated controller hook so components + * never call `mutate` directly inside a `useEffect`. + * Has a built-in 5-second timeout so the banner never blocks indefinitely. + */ +export const useEnsureIcebergInstalledOnMount = () => { + const { mutate, isLoading, data } = useEnsureIcebergInstalled(); + const hasRun = React.useRef(false); + const [timedOut, setTimedOut] = React.useState(false); + + React.useEffect(() => { + if (!hasRun.current) { + hasRun.current = true; + mutate(undefined, { + onError: () => { + // Suppress — install errors are not fatal for the DataLake UI + }, + }); + // Safety timeout — dismiss banner after 5 seconds regardless + const timer = setTimeout(() => setTimedOut(true), 5000); + return () => clearTimeout(timer); + } + return undefined; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { + // Hide banner if timed out or mutation resolved + isInstalling: isLoading && !timedOut, + installResult: data, + }; +}; diff --git a/src/renderer/screens/dataLake/index.tsx b/src/renderer/screens/dataLake/index.tsx index 15adc352..81783efb 100644 --- a/src/renderer/screens/dataLake/index.tsx +++ b/src/renderer/screens/dataLake/index.tsx @@ -1,6 +1,28 @@ import React, { useState, useEffect } from 'react'; -import { Typography, Box, Button, styled } from '@mui/material'; +import { + Typography, + Box, + Button, + styled, + Dialog, + DialogContent, + DialogTitle, + DialogActions, + DialogContentText, + Alert, + CircularProgress, + Paper, + Chip, +} from '@mui/material'; +import { + LocalFireDepartment, + Add, + ArrowBack, + Edit, + Delete, +} from '@mui/icons-material'; import { useLocation, useParams, useNavigate } from 'react-router-dom'; +import { toast } from 'react-toastify'; import { AppLayout } from '../../layouts'; import { DataLakeDashboard, @@ -12,6 +34,7 @@ import { DataLakeInstanceEditForm, DataLakeTableDetails, } from '../../components/dataLake'; +import { IcebergConnectionWizard } from '../../components/dataLake/IcebergConnectionWizard'; import { DataLakeCard } from '../../components/dataLakeCards'; import { useDuckLakeInstances, @@ -19,7 +42,23 @@ import { useDuckLakeInstance, useDeleteDuckLakeInstance, } from '../../controllers'; +import { + useListIcebergInstances, + useCreateIcebergInstance, + useGetIcebergInstance, + useUpdateIcebergInstance, + useDeleteIcebergInstance, + useEnsureIcebergInstalledOnMount, +} from '../../controllers/icebergDatalake.controller'; import { DuckLakeService } from '../../services'; +import type { + CreateIcebergInstanceDTO, + IcebergCatalogType, + IcebergCloudProvider, + IcebergInstanceConfig, + IcebergStorageType, +} from '../../../types/iceberg'; +import type { IcebergWizardData } from '../../components/dataLake/IcebergConnectionWizard'; const DataLake: React.FC = () => { const location = useLocation(); @@ -35,29 +74,44 @@ const DataLake: React.FC = () => { // State for type selection in new-instance flow const [selectedType, setSelectedType] = useState(); - // React Query hooks + // ── Iceberg UI state ─────────────────────────────────────────────────── + const [icebergEditId, setIcebergEditId] = useState(null); + const [icebergDeleteTarget, setIcebergDeleteTarget] = useState<{ + id: string; + name: string; + } | null>(null); + + // ── React Query — DuckLake ───────────────────────────────────────────── const instancesQuery = useDuckLakeInstances(); - // Add type field to instances for routing const instances = (instancesQuery.data || []).map((i) => ({ ...i, - type: 'duck-lake', // Hardcoded for now since only DuckLake exists + type: 'duck-lake', })); const createInstanceMutation = useCreateDuckLakeInstance(); - - // Mutations for instance actions const deleteMutation = useDeleteDuckLakeInstance(); - // Parse the current section from the pathname + // ── React Query — Iceberg ────────────────────────────────────────────── + const { data: icebergInstances = [] } = useListIcebergInstances(); + const createIcebergMutation = useCreateIcebergInstance(); + const updateIcebergMutation = useUpdateIcebergInstance(); + const deleteIcebergMutation = useDeleteIcebergInstance(); + const { data: editInstanceData, isLoading: editInstanceLoading } = + useGetIcebergInstance(icebergEditId ?? ''); + const activeIcebergId = type === 'iceberg' ? (instanceId ?? '') : ''; + const { + data: icebergInstance, + isLoading: icebergDetailLoading, + error: icebergDetailError, + } = useGetIcebergInstance(activeIcebergId); + + // ── Install gate (FE-05 pattern) ─────────────────────────────────────── + const { isInstalling } = useEnsureIcebergInstalledOnMount(); + + // ── Path parsing ─────────────────────────────────────────────────────── const pathSegments = location.pathname.split('/'); const currentSection = (() => { - if (pathSegments.includes('new-instance')) { - return 'new-instance'; - } - // Check for edit route pattern: /app/duck-lake/instances/:id/edit - if (pathSegments.includes('edit')) { - return 'edit-instance'; - } - // Check for table detail route pattern: /app/duck-lake/instances/:id/tables/:tableName + if (pathSegments.includes('new-instance')) return 'new-instance'; + if (pathSegments.includes('edit')) return 'edit-instance'; if ( pathSegments.includes('instances') && pathSegments.includes('tables') && @@ -65,31 +119,31 @@ const DataLake: React.FC = () => { ) { return 'table-detail'; } - // Check for tables route pattern: /app/duck-lake/instances/:id/tables if (pathSegments.includes('instances') && pathSegments.includes('tables')) { return 'instance-tables'; } if (pathSegments.includes('instances') && pathSegments.length > 4) { return 'instance-detail'; } - if (pathSegments.includes('instances')) { - return 'instances'; - } - if (pathSegments.includes('tables')) { - return 'tables'; - } - if (pathSegments.includes('history')) { - return 'history'; - } - if (pathSegments.includes('instance') && pathSegments.length > 4) { + if (pathSegments.includes('instances')) return 'instances'; + if (pathSegments.includes('tables')) return 'tables'; + if (pathSegments.includes('history')) return 'history'; + if (pathSegments.includes('instance') && pathSegments.length > 4) return 'instance-detail'; - } - if (pathSegments.includes('table') && pathSegments.length > 4) { + if (pathSegments.includes('table') && pathSegments.length > 4) return 'table-detail'; - } return pathSegments.pop() || 'dashboard'; })(); + // Pre-select lake type when navigating e.g. /new-instance?type=iceberg + useEffect(() => { + if (currentSection !== 'new-instance') return; + const typeParam = new URLSearchParams(location.search).get('type'); + if (typeParam === 'iceberg') { + setSelectedType('iceberg'); + } + }, [currentSection, location.search]); + // Define data lake types (UI only) const dataLakeTypes = [ { @@ -104,7 +158,7 @@ const DataLake: React.FC = () => { name: 'Apache Iceberg', description: 'Multi-engine, cloud-agnostic open standard', img: 'apacheIcebergLake' as const, - disabled: true, + disabled: false, // now enabled }, { id: 'delta', @@ -122,7 +176,6 @@ const DataLake: React.FC = () => { }, ]; - // Styled container for cards (reuse from addConnection pattern) const ConnectionCardsContainer = styled(Box)` display: flex; justify-content: center; @@ -133,24 +186,22 @@ const DataLake: React.FC = () => { margin: 0 auto; `; - // Get current instance ID from params or path const currentInstanceId = instanceId || pathSegments[pathSegments.indexOf('instance') + 1]; - // Get instance details if viewing a specific instance - const instanceQuery = useDuckLakeInstance( - currentSection === 'instance-detail' ? currentInstanceId || '' : '', - ); + const duckLakeInstanceId = + type !== 'iceberg' && currentSection === 'instance-detail' + ? currentInstanceId || '' + : ''; + + const instanceQuery = useDuckLakeInstance(duckLakeInstanceId); const currentInstance = instanceQuery.data; - // DuckLake connection lifecycle management - // Acquire connection when viewing instance details, tables, or table details - // Release connection when navigating away or component unmounts + // DuckLake connection lifecycle management (skip for Iceberg instances) useEffect(() => { let acquiredInstanceId: string | null = null; const acquireConnectionForInstance = async () => { - // Check if we're viewing any page that uses a DuckLake instance connection const instanceViewingSections = [ 'instance-detail', 'instance-tables', @@ -159,6 +210,7 @@ const DataLake: React.FC = () => { ]; if ( + type !== 'iceberg' && instanceViewingSections.includes(currentSection) && (instanceId || currentInstanceId) ) { @@ -176,35 +228,265 @@ const DataLake: React.FC = () => { acquireConnectionForInstance(); - // Cleanup: release connection when navigating away or component unmounts return () => { if (acquiredInstanceId) { DuckLakeService.releaseConnection(acquiredInstanceId); } }; - }, [currentSection, instanceId, currentInstanceId]); + }, [currentSection, instanceId, currentInstanceId, type]); + + // ── Iceberg handlers ─────────────────────────────────────────────────── + + const handleIcebergWizardComplete = async (wizardData: IcebergWizardData) => { + const dto: CreateIcebergInstanceDTO = { + name: wizardData.basics.name, + description: wizardData.basics.description, + catalogType: wizardData.catalog.catalogType as IcebergCatalogType, + catalogPath: wizardData.catalog.catalogPath, + endpoint: wizardData.catalog.endpoint, + catalogName: wizardData.catalog.catalogName, + databaseConnectionId: wizardData.catalog.databaseConnectionId, + accessToken: wizardData.catalog.accessToken, + catalogConnectionId: wizardData.catalog.polarisConnectionId, + catalogBucket: wizardData.catalog.polarisBucket, + catalogPrefix: wizardData.catalog.polarisPrefix, + storageType: wizardData.storage.storageType as IcebergStorageType, + localPath: wizardData.storage.localPath, + cloudProvider: wizardData.storage.cloudProvider as IcebergCloudProvider, + storageConnectionId: wizardData.storage.connectionId, + storageBucket: wizardData.storage.bucket, + storagePrefix: wizardData.storage.prefix, + }; + try { + const created = await createIcebergMutation.mutateAsync(dto); + setSelectedType(undefined); + toast.success('Iceberg instance created.'); + navigate(`/app/data-lake/iceberg/instances/${created.id}`); + } catch (err: any) { + // eslint-disable-next-line no-console + console.error(err); + toast.error(err?.message ?? 'Failed to create Iceberg instance.'); + } + }; + + const handleIcebergEditComplete = async (wizardData: IcebergWizardData) => { + if (!icebergEditId) return; + const dto: Partial = { + name: wizardData.basics.name, + description: wizardData.basics.description, + catalogType: wizardData.catalog.catalogType as IcebergCatalogType, + catalogPath: wizardData.catalog.catalogPath, + endpoint: wizardData.catalog.endpoint, + catalogName: wizardData.catalog.catalogName, + databaseConnectionId: wizardData.catalog.databaseConnectionId, + // Only send token if the user typed a new one; empty = preserve existing + ...(wizardData.catalog.accessToken + ? { accessToken: wizardData.catalog.accessToken } + : {}), + catalogConnectionId: wizardData.catalog.polarisConnectionId, + catalogBucket: wizardData.catalog.polarisBucket, + catalogPrefix: wizardData.catalog.polarisPrefix, + storageType: wizardData.storage.storageType as IcebergStorageType, + localPath: wizardData.storage.localPath, + cloudProvider: wizardData.storage.cloudProvider as IcebergCloudProvider, + storageConnectionId: wizardData.storage.connectionId, + storageBucket: wizardData.storage.bucket, + storagePrefix: wizardData.storage.prefix, + }; + try { + await updateIcebergMutation.mutateAsync({ id: icebergEditId, data: dto }); + setIcebergEditId(null); + toast.success('Iceberg instance updated.'); + } catch (err: any) { + // eslint-disable-next-line no-console + console.error(err); + toast.error(err?.message ?? 'Failed to update Iceberg instance.'); + } + }; - // Tables are now handled by DuckLakeTablesView component + const handleIcebergDeleteConfirm = async () => { + if (!icebergDeleteTarget) return; + try { + await deleteIcebergMutation.mutateAsync(icebergDeleteTarget.id); + if (activeIcebergId === icebergDeleteTarget.id) { + navigate('/app/data-lake/instances'); + } + setIcebergDeleteTarget(null); + toast.success('Iceberg instance deleted.'); + } catch (err: any) { + // eslint-disable-next-line no-console + console.error(err); + toast.error(err?.message ?? 'Failed to delete Iceberg instance.'); + } + }; + + // ── Render helpers ───────────────────────────────────────────────────── + + const renderIcebergInstanceDetail = (inst: IcebergInstanceConfig) => ( + + + + + + + {inst.name} + + + + {inst.description && ( + + {inst.description} + + )} + + + + + Catalog + + + + + Type + + {inst.catalogType} + + {inst.catalogPath && ( + + + Path + + + {inst.catalogPath} + + + )} + {inst.endpoint && ( + + + REST Endpoint + + {inst.endpoint} + + )} + {inst.catalogName && ( + + + Catalog Name + + {inst.catalogName} + + )} + + + + + + Storage + + + + + Type + + {inst.storageType} + + {inst.localPath && ( + + + Local Path + + + {inst.localPath} + + + )} + {inst.storageBucket && ( + + + Bucket + + {inst.storageBucket} + + )} + {inst.storagePrefix && ( + + + Prefix + + {inst.storagePrefix} + + )} + + + + }> + Namespace & table browsing coming soon (Plan 57c). Instance is + saved and ready. + + + + + + + + + ); - // Render content based on current section const renderContent = () => { switch (currentSection) { case 'dashboard': - return ; + return ( + + ); case 'instances': - return ; + return ( + + navigate('/app/data-lake/new-instance?type=iceberg') + } + onEditIceberg={(id) => setIcebergEditId(id)} + /> + ); case 'instance-tables': - // Show tables for a specific instance from route: /instances/:id/tables return ; case 'tables': - // Show tables for a specific instance if instanceId is in URL if (instanceId) { return ; } - // Otherwise show message to select an instance return ( { ); case 'new-instance': - // Step 1: Show type selection cards + // Step 1: type selection cards if (!selectedType) { return ( @@ -260,7 +542,7 @@ const DataLake: React.FC = () => { ); } - // Step 2: Show wizard for selected type (only duck-lake is implemented) + // Step 2: DuckLake wizard if (selectedType === 'duck-lake') { return ( { }; const newInstance = await createInstanceMutation.mutateAsync(createRequest); - // Navigate to type-specific route navigate( `/app/data-lake/duck-lake/instances/${newInstance.id}`, ); @@ -295,6 +576,21 @@ const DataLake: React.FC = () => { ); } + // Step 2: Iceberg wizard (inline, same pattern as DuckLake) + if (selectedType === 'iceberg') { + return ( + { + setSelectedType(undefined); + navigate('/app/data-lake/new-instance'); + }} + isLoading={createIcebergMutation.isLoading} + mode="create" + /> + ); + } + // Other types not yet implemented return ( @@ -312,6 +608,42 @@ const DataLake: React.FC = () => { return ; case 'instance-detail': + if (type === 'iceberg') { + if (icebergDetailLoading) { + return ( + + + + ); + } + + if (icebergDetailError || !icebergInstance) { + return ( + + + Iceberg Instance Not Found + + + The requested Iceberg instance could not be found. + + + + ); + } + + return renderIcebergInstanceDetail(icebergInstance); + } + if (instanceQuery.isLoading) { return ( @@ -362,7 +694,6 @@ const DataLake: React.FC = () => { ); case 'table-detail': - // Phase 8b: Render comprehensive table detail view return ; default: @@ -385,12 +716,93 @@ const DataLake: React.FC = () => { return ( } + sidebarContent={ + + } panelTitle="DataLake" > + {/* pyiceberg install banner */} + {isInstalling && ( + } + sx={{ mb: 2 }} + > + Installing pyiceberg into the managed Python environment… This may + take a moment. + + )} + {renderContent()} + + {/* ── Iceberg Edit Wizard Dialog ────────────────────────────────── */} + setIcebergEditId(null)} + maxWidth="md" + fullWidth + > + + {editInstanceLoading || !editInstanceData ? ( + + + + ) : ( + setIcebergEditId(null)} + isLoading={updateIcebergMutation.isLoading} + mode="edit" + initialData={editInstanceData} + /> + )} + + + + {/* ── Iceberg Delete Confirmation Dialog ───────────────────────── */} + setIcebergDeleteTarget(null)} + maxWidth="xs" + fullWidth + > + Delete Iceberg Instance + + + Delete Iceberg instance {icebergDeleteTarget?.name} + ? This cannot be undone. Keytar credentials for this instance will + also be removed. + + + + + + + ); }; diff --git a/src/renderer/services/iceberg.service.ts b/src/renderer/services/iceberg.service.ts index 8463242c..13434ab3 100644 --- a/src/renderer/services/iceberg.service.ts +++ b/src/renderer/services/iceberg.service.ts @@ -13,8 +13,13 @@ import type { IcebergFieldSpec, IcebergSnapshotInfo, IcebergPreviewResult, + IcebergLocalCatalogResult, + IcebergCapabilities, } from '../../types/iceberg'; +export const getIcebergCapabilities = (): Promise => + window.electron.ipcRenderer.invoke('iceberg:getCapabilities'); + export const listIcebergInstances = (): Promise => window.electron.ipcRenderer.invoke('iceberg:list'); @@ -91,7 +96,7 @@ export const previewIcebergTable = ( export const createIcebergMetadataFile = ( warehousePath: string, -): Promise => +): Promise => window.electron.ipcRenderer.invoke( 'iceberg:createMetadataFile', warehousePath, diff --git a/src/types/iceberg.ts b/src/types/iceberg.ts index 889885bf..5a945999 100644 --- a/src/types/iceberg.ts +++ b/src/types/iceberg.ts @@ -2,18 +2,54 @@ // Iceberg Data Lake — Phase 1: TypeScript type definitions export type IcebergCatalogType = - | 'file' + | 'sqlite' + | 'sql' + | 'rest' | 'polaris' - | 'glue' | 'hive' - | 'dynamodb' - | 'sql' - | 'bigquery' - | 'in-memory'; + | 'hadoop' + | 'glue' + | 'nessie'; + +export type IcebergStorageType = + | 'server-managed' + | 'local' + | 'nfs' + | 'hdfs' + | 'cloud'; + +export type IcebergCatalogAuthMode = + | 'none' + | 'token' + | 'oauth-client-credentials'; + +export type IcebergCloudProvider = + | 'aws' + | 'azure' + | 'gcs' + | 'minio' + | 'cloudflare-r2' + | 'backblaze-b2' + | 'rustfs' + | 'garage'; -export type IcebergStorageType = 'local' | 'cloud'; +export interface IcebergCatalogCapability { + type: IcebergCatalogType; + label: string; + pyicebergType: 'sql' | 'rest' | 'hive' | 'glue' | 'custom'; + enabled: boolean; + disabledReason?: string; + requiredFields: Array< + 'catalogPath' | 'endpoint' | 'catalogName' | 'databaseConnectionId' + >; + authModes: IcebergCatalogAuthMode[]; + allowedStorageTypes: IcebergStorageType[]; +} -export type IcebergCloudProvider = 's3-compatible' | 'aws' | 'azure' | 'gcs'; +export interface IcebergCapabilities { + catalogs: IcebergCatalogCapability[]; + cloudProviders: IcebergCloudProvider[]; +} export interface IcebergInstanceConfig { id: string; @@ -21,9 +57,10 @@ export interface IcebergInstanceConfig { description?: string; // Catalog catalogType: IcebergCatalogType; - catalogPath?: string; // file-based: path to metadata.json + catalogPath?: string; // local testing: path to the SQLite catalog database endpoint?: string; // REST: Polaris/Lakekeeper endpoint URL catalogName?: string; // REST: catalog name or warehouse + databaseConnectionId?: string; // Existing PostgreSQL/Neon connection catalogAccessTokenKey?: string; // keytar key: "iceberg-catalog-token-{id}" catalogConnectionId?: string; // Cloud Explorer connectionId for vended credentials catalogBucket?: string; @@ -46,6 +83,9 @@ export interface IcebergInstanceListItem { description?: string; catalogType: IcebergCatalogType; storageType: IcebergStorageType; + catalogPath?: string; + localPath?: string; + storageBucket?: string; createdAt: string; updatedAt: string; } @@ -89,6 +129,13 @@ export interface IcebergTestResult { error?: string; } +export interface IcebergLocalCatalogResult { + catalogPath: string; + warehousePath: string; + namespaces: string[][]; + tables: string[][]; +} + export interface IcebergTestCatalogParams { catalogType: IcebergCatalogType; catalogPath?: string; @@ -96,4 +143,6 @@ export interface IcebergTestCatalogParams { catalogName?: string; connectionId?: string; // resolves credentials from Cloud Explorer accessToken?: string; // raw token (not stored yet at test time) + databaseConnectionId?: string; + storageType?: IcebergStorageType; } diff --git a/src/types/ipc.ts b/src/types/ipc.ts index 9bc9b00d..767240de 100644 --- a/src/types/ipc.ts +++ b/src/types/ipc.ts @@ -347,6 +347,22 @@ export type DuckLakeChannels = | 'ducklake:connection:acquire' | 'ducklake:connection:release'; +export type IcebergChannels = + | 'iceberg:getCapabilities' + | 'iceberg:list' + | 'iceberg:get' + | 'iceberg:create' + | 'iceberg:update' + | 'iceberg:delete' + | 'iceberg:testCatalog' + | 'iceberg:listNamespaces' + | 'iceberg:listTables' + | 'iceberg:getSchema' + | 'iceberg:getSnapshots' + | 'iceberg:previewTable' + | 'iceberg:createMetadataFile' + | 'iceberg:ensureInstalled'; + export type LineageChannels = | 'lineage:getUpstream' | 'lineage:getDownstream' @@ -527,7 +543,8 @@ export type Channels = | StaticSiteChannels | FlowfileChannels | PipelineTemplatesChannels - | SecondBrainChannels; + | SecondBrainChannels + | IcebergChannels; export type ConfigureConnectionBody = { projectId?: string; diff --git a/tests/python/test_iceberg_bridge.py b/tests/python/test_iceberg_bridge.py new file mode 100644 index 00000000..26b47096 --- /dev/null +++ b/tests/python/test_iceberg_bridge.py @@ -0,0 +1,65 @@ +"""End-to-end verification for the local PyIceberg SQLite catalog.""" + +import importlib.util +import tempfile +import unittest +from pathlib import Path + +from pyiceberg.catalog import load_catalog +from pyiceberg.schema import Schema +from pyiceberg.types import LongType, NestedField, StringType + + +BRIDGE_PATH = ( + Path(__file__).resolve().parents[2] / "resources" / "python" / "iceberg_bridge.py" +) +SPEC = importlib.util.spec_from_file_location("iceberg_bridge", BRIDGE_PATH) +assert SPEC and SPEC.loader +ICEBERG_BRIDGE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(ICEBERG_BRIDGE) + + +class LocalCatalogBridgeTest(unittest.TestCase): + def test_create_reload_and_list_namespace_and_table(self) -> None: + with tempfile.TemporaryDirectory() as catalog_directory: + created = ICEBERG_BRIDGE.handle_create_metadata_file( + {"warehouse_path": catalog_directory} + ) + self.assertTrue(created["ok"]) + self.assertEqual(created["namespaces"], [["default"]]) + + properties = { + "type": "sql", + "uri": f"sqlite:///{created['metadata_path']}", + "warehouse": Path(created["warehouse_path"]).as_uri(), + } + catalog = load_catalog("local", **properties) + catalog.create_table( + ("default", "generated_data"), + schema=Schema( + NestedField(1, "id", LongType(), required=True), + NestedField(2, "name", StringType(), required=False), + ), + ) + catalog.close() + + namespaces = ICEBERG_BRIDGE.handle_list_namespaces( + { + "catalog_name": "local", + "catalog_properties": properties, + } + ) + tables = ICEBERG_BRIDGE.handle_list_tables( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + } + ) + + self.assertEqual(namespaces, {"ok": True, "namespaces": [["default"]]}) + self.assertEqual(tables, {"ok": True, "tables": ["generated_data"]}) + + +if __name__ == "__main__": + unittest.main() From c94fd3f5fad3527056dc7d15a0a12b32f115e473 Mon Sep 17 00:00:00 2001 From: Nuri Lacka Date: Sun, 9 Aug 2026 02:40:04 +0200 Subject: [PATCH 03/12] feat(iceberg): add management UI and connection health checks - add DuckLake-consistent Iceberg instance detail tabs and compact table listing - add routed table inspection for schema, data, snapshots, and properties - implement backend and UI connection testing for saved Iceberg instances - verify catalog discovery and warehouse metadata access through PyIceberg - normalize Arrow preview values into JSON-safe representations - validate preview filters and clamp row limits to 1000 - return table properties alongside schema information - add a shared Apache Iceberg icon across DataLake surfaces - refine the Iceberg connection wizard and SQL catalog requirements - update DataLake dashboard, sidebar, instances, routing, services, and controllers --- resources/python/iceberg_bridge.py | 43 +- .../icebergDatalake.ipcHandlers.ts | 4 + src/main/services/icebergDatalake.service.ts | 92 +- .../components/dataLake/DataLakeDashboard.tsx | 22 +- .../components/dataLake/DataLakeInstances.tsx | 61 +- .../components/dataLake/DataLakeSidebar.tsx | 8 +- .../dataLake/IcebergConnectionWizard.tsx | 150 ++- .../dataLake/IcebergInstanceListItem.tsx | 12 +- .../dataLake/iceberg/IcebergDetail.tsx | 1135 +++++++++++++++++ .../dataLake/iceberg/IcebergIcon.tsx | 15 + .../controllers/icebergDatalake.controller.ts | 28 +- src/renderer/screens/dataLake/index.tsx | 159 +-- src/renderer/services/iceberg.service.ts | 7 +- src/types/iceberg.ts | 10 + 14 files changed, 1477 insertions(+), 269 deletions(-) create mode 100644 src/renderer/components/dataLake/iceberg/IcebergDetail.tsx create mode 100644 src/renderer/components/dataLake/iceberg/IcebergIcon.tsx diff --git a/resources/python/iceberg_bridge.py b/resources/python/iceberg_bridge.py index 4835cdb1..328152e0 100644 --- a/resources/python/iceberg_bridge.py +++ b/resources/python/iceberg_bridge.py @@ -12,6 +12,8 @@ import json import os import sys +from datetime import date, datetime +from decimal import Decimal def resolve_env_vars(props: dict) -> dict: @@ -26,6 +28,21 @@ def resolve_env_vars(props: dict) -> dict: return result +def json_safe(value): + """Normalize Arrow scalar values before crossing the JSON bridge.""" + if isinstance(value, (datetime, date)): + return value.isoformat() + if isinstance(value, Decimal): + return str(value) + if isinstance(value, bytes): + return value.hex() + if isinstance(value, dict): + return {str(key): json_safe(item) for key, item in value.items()} + if isinstance(value, (list, tuple)): + return [json_safe(item) for item in value] + return value + + def handle_install_check(_cmd: dict) -> dict: """Check the complete runtime profile required by the implemented catalogs.""" try: @@ -40,13 +57,27 @@ def handle_install_check(_cmd: dict) -> dict: def handle_test_connection(cmd: dict) -> dict: - """Test that the catalog is reachable by listing its top-level namespaces.""" + """Test catalog access and, when a table exists, warehouse metadata access.""" try: from pyiceberg.catalog import load_catalog # noqa: PLC0415 props = resolve_env_vars(cmd.get("catalog_properties", {})) catalog = load_catalog(cmd["catalog_name"], **props) namespaces = catalog.list_namespaces() - return {"ok": True, "namespace_count": len(namespaces)} + table_count = 0 + warehouse_connected = None + for namespace in namespaces: + tables = catalog.list_tables(namespace) + table_count += len(tables) + if warehouse_connected is None and tables: + table = catalog.load_table(tables[0]) + next(iter(table.scan(limit=1).plan_files()), None) + warehouse_connected = True + return { + "ok": True, + "namespace_count": len(namespaces), + "table_count": table_count, + "warehouse_connected": warehouse_connected, + } except Exception as exc: # noqa: BLE001 return {"ok": False, "error": str(exc)} @@ -96,7 +127,11 @@ def handle_get_schema(cmd: dict) -> dict: "required": field.required, "doc": field.doc, }) - return {"ok": True, "fields": fields} + return { + "ok": True, + "fields": fields, + "properties": dict(table.properties), + } except Exception as exc: # noqa: BLE001 return {"ok": False, "error": str(exc)} @@ -144,7 +179,7 @@ def handle_preview_table(cmd: dict) -> dict: rows = arrow_table.to_pydict() # Convert column-oriented dict to row-oriented list of lists row_list = [ - [rows[col][i] for col in columns] + [json_safe(rows[col][i]) for col in columns] for i in range(len(arrow_table)) ] return { diff --git a/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts b/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts index 3e1fd1db..fed4a0ae 100644 --- a/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts +++ b/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts @@ -34,6 +34,10 @@ export const registerIcebergDatalakeHandlers = () => { IcebergDatalakeService.testCatalogConnection(params), ); + ipcMain.handle('iceberg:testInstance', (_e, id: string) => + IcebergDatalakeService.testInstanceConnection(id), + ); + ipcMain.handle('iceberg:listNamespaces', (_e, id: string, parent?) => IcebergDatalakeService.listNamespaces(id, parent), ); diff --git a/src/main/services/icebergDatalake.service.ts b/src/main/services/icebergDatalake.service.ts index 2b3491d5..1463cd93 100644 --- a/src/main/services/icebergDatalake.service.ts +++ b/src/main/services/icebergDatalake.service.ts @@ -25,6 +25,7 @@ import type { IcebergTestCatalogParams, IcebergTestResult, IcebergFieldSpec, + IcebergSchemaResult, IcebergSnapshotInfo, IcebergPreviewResult, IcebergLocalCatalogResult, @@ -61,7 +62,7 @@ export class IcebergDatalakeService { label: 'PostgreSQL / Neon', pyicebergType: 'sql', enabled: true, - requiredFields: ['databaseConnectionId'], + requiredFields: ['databaseConnectionId', 'catalogName'], authModes: ['none'], allowedStorageTypes: ['local', 'cloud'], }, @@ -713,7 +714,7 @@ export class IcebergDatalakeService { throw new Error(`ICEBERG_CATALOG_NOT_ENABLED: ${params.catalogType}`); } - await IcebergDatalakeService.runBridge( + const result = (await IcebergDatalakeService.runBridge( { command: 'test_connection', catalog_name: @@ -723,15 +724,74 @@ export class IcebergDatalakeService { catalog_properties: props, }, env, - ); - return { success: true }; + )) as Record; + return { + success: true, + catalogConnected: true, + warehouseConnected: + typeof result.warehouse_connected === 'boolean' + ? result.warehouse_connected + : undefined, + namespaceCount: Number(result.namespace_count ?? 0), + tableCount: Number(result.table_count ?? 0), + checkedAt: new Date().toISOString(), + }; } catch (error) { // eslint-disable-next-line no-console console.error( '[IcebergDatalakeService] testCatalogConnection error:', error, ); - return { success: false, error: String(error) }; + return { + success: false, + catalogConnected: false, + warehouseConnected: false, + checkedAt: new Date().toISOString(), + error: String(error), + }; + } + } + + static async testInstanceConnection(id: string): Promise { + try { + const instance = await IcebergDatalakeService.getInstance(id); + const { props, env } = + await IcebergDatalakeService.buildCatalogProperties(instance); + const result = (await IcebergDatalakeService.runBridge( + { + command: 'test_connection', + catalog_name: + instance.catalogType === 'sqlite' + ? 'local' + : (instance.catalogName ?? id), + catalog_properties: props, + }, + env, + )) as Record; + return { + success: true, + catalogConnected: true, + warehouseConnected: + typeof result.warehouse_connected === 'boolean' + ? result.warehouse_connected + : undefined, + namespaceCount: Number(result.namespace_count ?? 0), + tableCount: Number(result.table_count ?? 0), + checkedAt: new Date().toISOString(), + }; + } catch (error) { + // eslint-disable-next-line no-console + console.error( + '[IcebergDatalakeService] testInstanceConnection error:', + error, + ); + return { + success: false, + catalogConnected: false, + warehouseConnected: false, + checkedAt: new Date().toISOString(), + error: String(error), + }; } } @@ -879,7 +939,7 @@ export class IcebergDatalakeService { id: string, namespace: string[], table: string, - ): Promise { + ): Promise { try { const instance = await IcebergDatalakeService.getInstance(id); const { props, env } = @@ -897,7 +957,11 @@ export class IcebergDatalakeService { }, env, )) as Record; - return (result.fields as IcebergFieldSpec[]) ?? []; + return { + fields: (result.fields as IcebergFieldSpec[]) ?? [], + properties: + (result.properties as Record | undefined) ?? {}, + }; } catch (error) { // eslint-disable-next-line no-console console.error('[IcebergDatalakeService] getTableSchema error:', error); @@ -943,6 +1007,16 @@ export class IcebergDatalakeService { rowFilter?: string, ): Promise { try { + const safeLimit = Math.min( + Math.max(1, Math.floor(Number.isFinite(limit) ? limit : 100)), + 1000, + ); + const safeRowFilter = rowFilter?.trim(); + if (safeRowFilter && !/^[A-Za-z0-9_\s.'"<>=!()-]+$/.test(safeRowFilter)) { + throw new Error( + 'Invalid row filter. Use column names, literals, comparison operators, AND/OR, and parentheses only.', + ); + } const instance = await IcebergDatalakeService.getInstance(id); const { props, env } = await IcebergDatalakeService.buildCatalogProperties(instance); @@ -956,8 +1030,8 @@ export class IcebergDatalakeService { catalog_properties: props, namespace, table, - limit: Math.min(Math.max(1, limit), 10000), // clamp per BE-02 - row_filter: rowFilter, + limit: safeLimit, + row_filter: safeRowFilter || undefined, }, env, )) as Record; diff --git a/src/renderer/components/dataLake/DataLakeDashboard.tsx b/src/renderer/components/dataLake/DataLakeDashboard.tsx index 512819db..70f084f6 100644 --- a/src/renderer/components/dataLake/DataLakeDashboard.tsx +++ b/src/renderer/components/dataLake/DataLakeDashboard.tsx @@ -17,8 +17,8 @@ import { Dashboard, Add, Folder, - LocalFireDepartment, } from '@mui/icons-material'; +import { IcebergIcon } from './iceberg/IcebergIcon'; import { cloudStorageImages, databaseIcons, @@ -159,11 +159,7 @@ export const DataLakeDashboard: React.FC = ({ const getCatalogIcon = (type: string) => { if (type.startsWith('iceberg-')) { - return ( - - ); + return ; } let iconSrc; @@ -412,7 +408,11 @@ export const DataLakeDashboard: React.FC = ({ sx={{ pb: 1 }} /> - + 0 @@ -449,7 +449,11 @@ export const DataLakeDashboard: React.FC = ({ sx={{ pb: 1 }} /> - + 0 @@ -591,7 +595,7 @@ export const DataLakeDashboard: React.FC = ({ sx={{ display: 'flex', alignItems: 'center', gap: 1 }} > {instance.lakeType === 'iceberg' ? ( - + ) : ( getStorageIconForInstance(instance.dataPath) )} diff --git a/src/renderer/components/dataLake/DataLakeInstances.tsx b/src/renderer/components/dataLake/DataLakeInstances.tsx index 5d4048de..1cb0d14c 100644 --- a/src/renderer/components/dataLake/DataLakeInstances.tsx +++ b/src/renderer/components/dataLake/DataLakeInstances.tsx @@ -17,13 +17,7 @@ import { CircularProgress, Alert, } from '@mui/material'; -import { - Edit, - Delete, - Storage, - Refresh, - LocalFireDepartment, -} from '@mui/icons-material'; +import { Edit, Delete, Storage, Refresh } from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; import moment from 'moment'; import { @@ -36,6 +30,7 @@ import { } from '../../controllers/icebergDatalake.controller'; import { ConfirmationModal } from '../modals/confirmationModal'; import type { IcebergInstanceListItem } from '../../../types/iceberg'; +import { IcebergIcon } from './iceberg/IcebergIcon'; type DataLakeTableRow = | { @@ -114,21 +109,23 @@ export const DataLakeInstances: React.FC = ({ updatedAt: instance.updatedAt, })); - const icebergRows: DataLakeTableRow[] = icebergInstances.map((instance) => ({ - id: instance.id, - name: instance.name, - description: instance.description, - lakeType: 'iceberg', - catalogType: instance.catalogType.toUpperCase(), - dataPath: - instance.localPath || - instance.catalogPath || - instance.storageBucket || - instance.storageType, - createdAt: instance.createdAt, - updatedAt: instance.updatedAt, - icebergInstance: instance, - })); + const icebergRows: DataLakeTableRow[] = icebergInstances.map( + (instance) => ({ + id: instance.id, + name: instance.name, + description: instance.description, + lakeType: 'iceberg', + catalogType: instance.catalogType.toUpperCase(), + dataPath: + instance.localPath || + instance.catalogPath || + instance.storageBucket || + instance.storageType, + createdAt: instance.createdAt, + updatedAt: instance.updatedAt, + icebergInstance: instance, + }), + ); return [...duckRows, ...icebergRows].sort( (a, b) => @@ -236,7 +233,7 @@ export const DataLakeInstances: React.FC = ({
+ + + Namespace + Table + Snapshot ID + Operation + Committed At + + + + {namespaces.map((namespace) => ( + + ))} + +
+
+); + +const TableDetail: React.FC<{ + instance: IcebergInstanceConfig; + selection: SelectedTable; +}> = ({ instance, selection }) => { + const navigate = useNavigate(); + const [tab, setTab] = React.useState(0); + const [limit, setLimit] = React.useState(100); + const [rowFilter, setRowFilter] = React.useState(''); + const [appliedFilter, setAppliedFilter] = React.useState(''); + const [filterError, setFilterError] = React.useState(null); + const schemaQuery = useGetIcebergSchema( + instance.id, + selection.namespace, + selection.table, + ); + const snapshotsQuery = useGetIcebergSnapshots( + instance.id, + selection.namespace, + selection.table, + ); + const previewQuery = useIcebergTablePreview( + instance.id, + selection.namespace, + selection.table, + limit, + appliedFilter, + tab === 2, + ); + + const applyFilter = () => { + const value = rowFilter.trim(); + if (value && !/^[A-Za-z0-9_\s.'"<>=!()-]+$/.test(value)) { + setFilterError('The filter contains unsupported characters.'); + return; + } + setFilterError(null); + setAppliedFilter(value); + if (value === appliedFilter) previewQuery.refetch(); + }; + + const renderOverview = () => { + const snapshots = snapshotsQuery.data ?? []; + const currentSnapshot = snapshots[snapshots.length - 1]; + return ( + + + + + Table Information + + + + + + + + + + + + Snapshot Status + + + + + + + + + + ); + }; + + const renderSchema = () => { + if (schemaQuery.isLoading) { + return ; + } + if (schemaQuery.isError) { + return {errorMessage(schemaQuery.error)}; + } + const fields = schemaQuery.data?.fields ?? []; + if (fields.length === 0) { + return No schema fields found.; + } + return ( + + + + + Field ID + Name + Type + Required + Description + + + + {fields.map((field) => ( + + {field.fieldId} + {field.name} + {field.type} + {field.required ? 'Yes' : 'No'} + {field.doc ?? '—'} + + ))} + +
+
+ ); + }; + + const renderHistory = () => { + if (snapshotsQuery.isLoading) { + return ; + } + if (snapshotsQuery.isError) { + return ( + {errorMessage(snapshotsQuery.error)} + ); + } + const snapshots = snapshotsQuery.data ?? []; + if (snapshots.length === 0) { + return No snapshots found.; + } + return ( + + + + + Snapshot ID + Operation + Committed At + Added Records + Total Records + + + + {[...snapshots].reverse().map((snapshot, index) => ( + + + …{snapshot.snapshotId.slice(-8)} + + + + + + {new Date(Number(snapshot.committedAt)).toLocaleString()} + + + {snapshot.summary['added-records'] ?? '—'} + + + {snapshot.summary['total-records'] ?? '—'} + + + ))} + +
+
+ ); + }; + + const renderPreview = () => ( + + + setRowFilter(event.target.value)} + error={!!filterError} + helperText={filterError ?? 'Example: id >= 2'} + slotProps={{ + input: { + sx: { height: '32px', fontSize: '0.8125rem' }, + }, + inputLabel: { + sx: { fontSize: '0.8125rem' }, + }, + formHelperText: { + sx: { mt: 0.5, fontSize: '0.6875rem' }, + }, + }} + sx={{ + flex: 1, + '& .MuiInputBase-input': { py: 0.5 }, + '& .MuiOutlinedInput-root': { minHeight: '32px' }, + }} + /> + + Limit + + + + + {previewQuery.isFetching && } + {previewQuery.isError && ( + {errorMessage(previewQuery.error)} + )} + {previewQuery.data && ( + + + + + {previewQuery.data.columns.map((column) => ( + {column} + ))} + + + + {previewQuery.data.rows.map((row, rowIndex) => ( + + {row.map((value, columnIndex) => ( + + {formatCellValue(value)} + + ))} + + ))} + +
+
+ )} +
+ ); + + const renderProperties = () => { + if (schemaQuery.isLoading) { + return ; + } + const properties = Object.entries(schemaQuery.data?.properties ?? {}); + if (properties.length === 0) { + return No table properties defined.; + } + return ( + + + {properties.map(([key, value]) => ( + + {key} + {value} + + ))} + +
+ ); + }; + + return ( + + + + + navigate(`/app/data-lake/iceberg/instances/${instance.id}`) + } + > + + + + + + + {selection.table} + + + Namespace: {selection.namespace.join('.')} • Apache Iceberg + + + {snapshotsQuery.data?.length ? ( + + ) : null} + + setTab(value)} + sx={{ borderBottom: 1, borderColor: 'divider' }} + > + } label="Overview" iconPosition="start" /> + } label="Schema" iconPosition="start" /> + } label="Data" iconPosition="start" /> + } label="History" iconPosition="start" /> + } label="Properties" iconPosition="start" /> + + + {tab === 0 && renderOverview()} + {tab === 1 && renderSchema()} + {tab === 2 && renderPreview()} + {tab === 3 && renderHistory()} + {tab === 4 && renderProperties()} + + + ); +}; + +export const IcebergTableDetails: React.FC = () => { + const { instanceId = '', tableName = '' } = useParams<{ + instanceId: string; + tableName: string; + }>(); + const instanceQuery = useGetIcebergInstance(instanceId); + const identifier = decodeURIComponent(tableName); + const parts = identifier.split('.').filter(Boolean); + const table = parts.pop() ?? ''; + const namespace = parts; + + if (instanceQuery.isLoading) { + return ; + } + if ( + instanceQuery.isError || + !instanceQuery.data || + !table || + !namespace.length + ) { + return Iceberg table could not be loaded.; + } + return ( + + ); +}; + +export const IcebergDetail: React.FC = ({ + instance, + onEdit, + onDelete, +}) => { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const namespacesQuery = useListIcebergNamespaces(instance.id); + const testInstanceMutation = useTestIcebergInstance(); + const [currentTab, setCurrentTab] = React.useState(0); + const [tableFilter, setTableFilter] = React.useState(''); + + const refresh = async () => { + await queryClient.invalidateQueries(['iceberg', 'namespaces', instance.id]); + await queryClient.invalidateQueries(['iceberg', 'tables', instance.id]); + await queryClient.invalidateQueries(['iceberg', 'schema', instance.id]); + await queryClient.invalidateQueries(['iceberg', 'snapshots', instance.id]); + await queryClient.invalidateQueries(['iceberg', 'preview', instance.id]); + }; + + const openTable = (selection: SelectedTable) => { + const identifier = [...selection.namespace, selection.table].join('.'); + navigate( + `/app/data-lake/iceberg/instances/${instance.id}/tables/${encodeURIComponent(identifier)}`, + ); + }; + + const testConnection = async () => { + try { + const result = await testInstanceMutation.mutateAsync(instance.id); + if (result.success) { + toast.success('Iceberg catalog connection successful.'); + return; + } + toast.error(result.error ?? 'Iceberg catalog connection failed.'); + } catch (error) { + toast.error(errorMessage(error)); + } + }; + + const testResult = testInstanceMutation.data; + const testIndicatorColor = (() => { + if (testInstanceMutation.isLoading) return 'warning.main'; + if (!testResult) return 'grey.500'; + return testResult.success ? 'success.main' : 'error.main'; + })(); + + const connectionStatusIcon = (healthy?: boolean) => { + if (healthy === true) { + return ; + } + if (healthy === false) { + return ; + } + return ; + }; + let catalogStatusLabel = 'Not tested'; + if (testResult?.catalogConnected === true) { + catalogStatusLabel = 'Connected'; + } else if (testResult?.catalogConnected === false) { + catalogStatusLabel = 'Connection failed'; + } + let warehouseStatusLabel = 'Not verified'; + if (testResult?.warehouseConnected === true) { + warehouseStatusLabel = 'Accessible'; + } else if (testResult?.warehouseConnected === false) { + warehouseStatusLabel = 'Access failed'; + } + + const catalogIcon = (() => { + if (instance.catalogType === 'sql') { + return ( + + ); + } + if (instance.catalogType === 'sqlite') { + return ; + } + return ; + })(); + + const providerIcon = instance.cloudProvider + ? cloudStorageImages[instance.cloudProvider] + : undefined; + const warehouseIcon = (() => { + if (instance.storageType === 'local' || instance.storageType === 'nfs') { + return ; + } + return ; + })(); + let providerRowIcon = ; + if (providerIcon) { + providerRowIcon = ( + + ); + } else if (instance.storageType === 'local') { + providerRowIcon = ; + } + + const renderTables = () => { + if (namespacesQuery.isLoading) { + return ; + } + if (namespacesQuery.isError) { + return ( + {errorMessage(namespacesQuery.error)} + ); + } + if (namespacesQuery.data?.length === 0) { + return No namespaces or tables found.; + } + return ( + <> + setTableFilter(event.target.value)} + placeholder="Search by name or schema…" + slotProps={{ + input: { + startAdornment: ( + + + + ), + sx: { fontSize: '0.8125rem', height: '32px' }, + }, + }} + sx={{ + width: 280, + mb: 2, + '& .MuiInputBase-input': { + paddingTop: '2px', + paddingBottom: '2px', + }, + '& .MuiOutlinedInput-root': { + minHeight: '32px', + }, + }} + /> + + + + + Name + Type + Schema + Rows + Size + Updated + Created + Actions + + + + {namespacesQuery.data?.map((namespace) => ( + + ))} + +
+
+ + ); + }; + + return ( + + + + + + {instance.name} + + + Apache Iceberg • {instance.catalogType.toUpperCase()} Catalog + + + + + + + + setCurrentTab(value)} + sx={{ borderBottom: 1, borderColor: 'divider' }} + > + + + + + + {currentTab === 0 && renderTables()} + {currentTab === 1 && ( + + + + + + + Health Status + + + + + + + {connectionStatusIcon(testResult?.catalogConnected)} + + + + + + {connectionStatusIcon(testResult?.warehouseConnected)} + + + + + + + + {testResult?.error && ( + + {testResult.error} + + )} + + + + + + + Catalog Configuration + + + + + {catalogIcon} + + + + + + + + + + + + + + + + + + + + + + + Warehouse Configuration + + + + + {warehouseIcon} + + + + + + {providerRowIcon} + + + + + + + + + + + + + + )} + {currentTab === 2 && ( + + )} + + + + ); +}; diff --git a/src/renderer/components/dataLake/iceberg/IcebergIcon.tsx b/src/renderer/components/dataLake/iceberg/IcebergIcon.tsx new file mode 100644 index 00000000..bc4ef33b --- /dev/null +++ b/src/renderer/components/dataLake/iceberg/IcebergIcon.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Box } from '@mui/material'; +import { icons } from '../../../../../assets'; + +export const IcebergIcon: React.FC<{ + size?: number; + alt?: string; +}> = ({ size = 18, alt = 'Apache Iceberg' }) => ( + +); diff --git a/src/renderer/controllers/icebergDatalake.controller.ts b/src/renderer/controllers/icebergDatalake.controller.ts index 93c4fc06..3dafa53a 100644 --- a/src/renderer/controllers/icebergDatalake.controller.ts +++ b/src/renderer/controllers/icebergDatalake.controller.ts @@ -35,14 +35,14 @@ export const useListIcebergNamespaces = (id: string, parent?: string[]) => useQuery( ['iceberg', 'namespaces', id, parent ?? []], () => icebergService.listIcebergNamespaces(id, parent), - { enabled: !!id }, + { enabled: !!id, staleTime: 60_000 }, ); export const useListIcebergTables = (id: string, namespace: string[]) => useQuery( ['iceberg', 'tables', id, namespace], () => icebergService.listIcebergTables(id, namespace), - { enabled: !!id && namespace.length > 0 }, + { enabled: !!id && namespace.length > 0, staleTime: 60_000 }, ); export const useGetIcebergSchema = ( @@ -67,6 +67,27 @@ export const useGetIcebergSnapshots = ( { enabled: !!id && !!table }, ); +export const useIcebergTablePreview = ( + id: string, + namespace: string[], + table: string, + limit: number, + rowFilter: string, + enabled: boolean, +) => + useQuery( + ['iceberg', 'preview', id, namespace, table, limit, rowFilter], + () => + icebergService.previewIcebergTable( + id, + namespace, + table, + limit, + rowFilter || undefined, + ), + { enabled: enabled && !!id && !!table, staleTime: 30_000 }, + ); + // ───────────────────────────────────────────── // Mutations // ───────────────────────────────────────────── @@ -108,6 +129,9 @@ export const useTestIcebergCatalog = () => icebergService.testIcebergCatalog(params), ); +export const useTestIcebergInstance = () => + useMutation((id: string) => icebergService.testIcebergInstance(id)); + export const useCreateIcebergMetadataFile = () => useMutation((warehousePath: string) => icebergService.createIcebergMetadataFile(warehousePath), diff --git a/src/renderer/screens/dataLake/index.tsx b/src/renderer/screens/dataLake/index.tsx index 81783efb..c1497651 100644 --- a/src/renderer/screens/dataLake/index.tsx +++ b/src/renderer/screens/dataLake/index.tsx @@ -11,16 +11,7 @@ import { DialogContentText, Alert, CircularProgress, - Paper, - Chip, } from '@mui/material'; -import { - LocalFireDepartment, - Add, - ArrowBack, - Edit, - Delete, -} from '@mui/icons-material'; import { useLocation, useParams, useNavigate } from 'react-router-dom'; import { toast } from 'react-toastify'; import { AppLayout } from '../../layouts'; @@ -35,6 +26,10 @@ import { DataLakeTableDetails, } from '../../components/dataLake'; import { IcebergConnectionWizard } from '../../components/dataLake/IcebergConnectionWizard'; +import { + IcebergDetail, + IcebergTableDetails, +} from '../../components/dataLake/iceberg/IcebergDetail'; import { DataLakeCard } from '../../components/dataLakeCards'; import { useDuckLakeInstances, @@ -323,141 +318,11 @@ const DataLake: React.FC = () => { // ── Render helpers ───────────────────────────────────────────────────── const renderIcebergInstanceDetail = (inst: IcebergInstanceConfig) => ( - - - - - - - {inst.name} - - - - {inst.description && ( - - {inst.description} - - )} - - - - - Catalog - - - - - Type - - {inst.catalogType} - - {inst.catalogPath && ( - - - Path - - - {inst.catalogPath} - - - )} - {inst.endpoint && ( - - - REST Endpoint - - {inst.endpoint} - - )} - {inst.catalogName && ( - - - Catalog Name - - {inst.catalogName} - - )} - - - - - - Storage - - - - - Type - - {inst.storageType} - - {inst.localPath && ( - - - Local Path - - - {inst.localPath} - - - )} - {inst.storageBucket && ( - - - Bucket - - {inst.storageBucket} - - )} - {inst.storagePrefix && ( - - - Prefix - - {inst.storagePrefix} - - )} - - - - }> - Namespace & table browsing coming soon (Plan 57c). Instance is - saved and ready. - - - - - - - - + setIcebergEditId(inst.id)} + onDelete={() => setIcebergDeleteTarget({ id: inst.id, name: inst.name })} + /> ); const renderContent = () => { @@ -694,7 +559,11 @@ const DataLake: React.FC = () => { ); case 'table-detail': - return ; + return type === 'iceberg' ? ( + + ) : ( + + ); default: return ( diff --git a/src/renderer/services/iceberg.service.ts b/src/renderer/services/iceberg.service.ts index 13434ab3..13db68a4 100644 --- a/src/renderer/services/iceberg.service.ts +++ b/src/renderer/services/iceberg.service.ts @@ -10,7 +10,7 @@ import type { IcebergInstanceConfig, IcebergTestCatalogParams, IcebergTestResult, - IcebergFieldSpec, + IcebergSchemaResult, IcebergSnapshotInfo, IcebergPreviewResult, IcebergLocalCatalogResult, @@ -47,6 +47,9 @@ export const testIcebergCatalog = ( ): Promise => window.electron.ipcRenderer.invoke('iceberg:testCatalog', params); +export const testIcebergInstance = (id: string): Promise => + window.electron.ipcRenderer.invoke('iceberg:testInstance', id); + export const listIcebergNamespaces = ( id: string, parent?: string[], @@ -63,7 +66,7 @@ export const getIcebergTableSchema = ( id: string, namespace: string[], table: string, -): Promise => +): Promise => window.electron.ipcRenderer.invoke('iceberg:getSchema', id, namespace, table); export const getIcebergTableSnapshots = ( diff --git a/src/types/iceberg.ts b/src/types/iceberg.ts index 5a945999..1ff45d76 100644 --- a/src/types/iceberg.ts +++ b/src/types/iceberg.ts @@ -109,6 +109,11 @@ export interface IcebergFieldSpec { doc?: string; } +export interface IcebergSchemaResult { + fields: IcebergFieldSpec[]; + properties: Record; +} + export interface IcebergSnapshotInfo { snapshotId: string; parentId?: string; @@ -127,6 +132,11 @@ export interface IcebergPreviewResult { export interface IcebergTestResult { success: boolean; error?: string; + catalogConnected?: boolean; + warehouseConnected?: boolean; + namespaceCount?: number; + tableCount?: number; + checkedAt?: string; } export interface IcebergLocalCatalogResult { From f996778627b16fbea98efef89487c78469a9b9b4 Mon Sep 17 00:00:00 2001 From: Nuri Lacka Date: Sun, 9 Aug 2026 15:13:41 +0200 Subject: [PATCH 04/12] feat(iceberg): add Polaris OAuth and persisted-table support - add OAuth2 client-credentials authentication for REST and Polaris catalogs - support client ID, client secret, token endpoint, and optional OAuth scope - store OAuth client secrets securely in keytar - exclude raw catalog secrets from database persistence - redact OAuth, SQL, and warehouse credentials from bridge errors - validate authentication and catalog compatibility before Python execution - add Polaris and MinIO persisted-table acceptance coverage - verify create, append, close, reload, listing, schema, snapshots, and preview - verify server-returned warehouse properties override client S3 configuration - add focused service tests for capabilities, validation, persistence, and redaction --- src/main/services/icebergDatalake.service.ts | 192 ++++++++++++++++-- .../dataLake/IcebergConnectionWizard.tsx | 147 +++++++++++++- src/renderer/screens/dataLake/index.tsx | 12 ++ src/types/iceberg.ts | 11 + tests/python/test_iceberg_rest_catalog.py | 115 +++++++++++ .../services/icebergDatalake.service.test.ts | 119 +++++++++++ 6 files changed, 578 insertions(+), 18 deletions(-) create mode 100644 tests/python/test_iceberg_rest_catalog.py create mode 100644 tests/unit/main/services/icebergDatalake.service.test.ts diff --git a/src/main/services/icebergDatalake.service.ts b/src/main/services/icebergDatalake.service.ts index 1463cd93..10e02e46 100644 --- a/src/main/services/icebergDatalake.service.ts +++ b/src/main/services/icebergDatalake.service.ts @@ -72,7 +72,7 @@ export class IcebergDatalakeService { pyicebergType: 'rest', enabled: true, requiredFields: ['endpoint', 'catalogName'], - authModes: ['none', 'token'], + authModes: ['none', 'token', 'oauth-client-credentials'], allowedStorageTypes: ['server-managed'], }, { @@ -81,7 +81,7 @@ export class IcebergDatalakeService { pyicebergType: 'rest', enabled: true, requiredFields: ['endpoint', 'catalogName'], - authModes: ['none', 'token'], + authModes: ['none', 'token', 'oauth-client-credentials'], allowedStorageTypes: ['server-managed'], }, ...( @@ -163,6 +163,37 @@ export class IcebergDatalakeService { return 'python3'; } + private static redactBridgeSecrets( + message: string, + env: Record, + ): string { + const secrets = new Set(); + Object.entries(env).forEach(([key, value]) => { + if (!value) return; + secrets.add(value); + if (key === 'ICEBERG_OAUTH_CREDENTIAL') { + secrets.add(value.slice(value.indexOf(':') + 1)); + } + if (key === 'ICEBERG_SQL_CATALOG_URI') { + try { + const parsed = new URL( + value.replace(/^postgresql\+psycopg2:/, 'postgresql:'), + ); + if (parsed.password) secrets.add(decodeURIComponent(parsed.password)); + } catch { + // The complete URI is still redacted below. + } + } + }); + return [...secrets] + .filter((secret) => secret.length >= 4) + .sort((left, right) => right.length - left.length) + .reduce( + (redacted, secret) => redacted.split(secret).join('[REDACTED]'), + message, + ); + } + /** * Spawns the Python bridge, writes command JSON to stdin, reads result from stdout. * BE-04: all async values resolved BEFORE entering new Promise constructor. @@ -192,12 +223,26 @@ export class IcebergDatalakeService { try { const result = JSON.parse(stdout) as Record; if (!result.ok) { - reject(new Error((result.error as string) ?? 'Bridge error')); + reject( + new Error( + IcebergDatalakeService.redactBridgeSecrets( + (result.error as string) ?? 'Bridge error', + env, + ), + ), + ); } else { resolve(result); } } catch { - reject(new Error(`Bridge parse error (exit ${code}): ${stderr}`)); + reject( + new Error( + IcebergDatalakeService.redactBridgeSecrets( + `Bridge parse error (exit ${code}): ${stderr}`, + env, + ), + ), + ); } }); child.on('error', (err: Error) => reject(err)); @@ -262,6 +307,26 @@ export class IcebergDatalakeService { ); } } + if ( + instance.catalogAuthMode === 'oauth-client-credentials' && + instance.oauthClientId && + instance.oauthClientSecretKey + ) { + const clientSecret = await secureStorage.getCredential( + instance.oauthClientSecretKey, + ); + if (!clientSecret) { + throw new Error('ICEBERG_OAUTH_SECRET_NOT_FOUND'); + } + props.credential = '__ENV:ICEBERG_OAUTH_CREDENTIAL'; + env.ICEBERG_OAUTH_CREDENTIAL = `${instance.oauthClientId}:${clientSecret}`; + if (instance.oauthServerUri) { + props['oauth2-server-uri'] = instance.oauthServerUri; + } + if (instance.oauthScope) props.scope = instance.oauthScope; + delete props.token; + delete env.ICEBERG_ACCESS_TOKEN; + } break; default: @@ -480,6 +545,47 @@ export class IcebergDatalakeService { } } + private static validateCatalogAuthentication(config: { + catalogType: IcebergInstanceConfig['catalogType']; + catalogAuthMode?: IcebergInstanceConfig['catalogAuthMode']; + accessToken?: string; + catalogAccessTokenKey?: string; + oauthClientId?: string; + oauthClientSecret?: string; + oauthClientSecretKey?: string; + oauthServerUri?: string; + }): void { + const mode = config.catalogAuthMode ?? 'none'; + if ( + mode !== 'none' && + config.catalogType !== 'rest' && + config.catalogType !== 'polaris' && + config.catalogType !== 'nessie' + ) { + throw new Error( + `ICEBERG_AUTH_MODE_NOT_ALLOWED: ${config.catalogType}/${mode}`, + ); + } + if ( + mode === 'token' && + !config.accessToken && + !config.catalogAccessTokenKey + ) { + throw new Error('ICEBERG_ACCESS_TOKEN_REQUIRED'); + } + if (mode === 'oauth-client-credentials') { + if (!config.oauthClientId?.trim() || config.oauthClientId.includes(':')) { + throw new Error('ICEBERG_OAUTH_CLIENT_ID_INVALID'); + } + if (!config.oauthClientSecret && !config.oauthClientSecretKey) { + throw new Error('ICEBERG_OAUTH_CLIENT_SECRET_REQUIRED'); + } + if (!config.oauthServerUri?.trim()) { + throw new Error('ICEBERG_OAUTH_SERVER_URI_REQUIRED'); + } + } + } + static getCapabilities(): IcebergCapabilities { return { catalogs: IcebergDatalakeService.catalogCapabilities.map((item) => ({ @@ -549,6 +655,7 @@ export class IcebergDatalakeService { ): Promise { try { IcebergDatalakeService.validateCatalogWarehousePair(data); + IcebergDatalakeService.validateCatalogAuthentication(data); const id = uuidv4(); const now = new Date().toISOString(); @@ -561,15 +668,30 @@ export class IcebergDatalakeService { ); } - // Strip the raw accessToken before persisting - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { accessToken: _accessTokenCreate, ...rest } = data; + let oauthClientSecretKey: string | undefined; + if (data.oauthClientSecret) { + oauthClientSecretKey = `iceberg-oauth-secret-${id}`; + await secureStorage.setCredential( + oauthClientSecretKey, + data.oauthClientSecret, + ); + } + + // Strip raw secrets before persisting + /* eslint-disable @typescript-eslint/no-unused-vars */ + const { + accessToken: _accessTokenCreate, + oauthClientSecret: _oauthClientSecretCreate, + ...rest + } = data; + /* eslint-enable @typescript-eslint/no-unused-vars */ const newInstance: IcebergInstanceConfig = { ...rest, id, catalogAccessTokenKey: catalogAccessTokenKey ?? data.catalogAccessTokenKey, + oauthClientSecretKey: oauthClientSecretKey ?? data.oauthClientSecretKey, createdAt: now, updatedAt: now, }; @@ -595,10 +717,12 @@ export class IcebergDatalakeService { const idx = instances.findIndex((i) => i.id === id); if (idx < 0) throw new Error(`Iceberg instance not found: ${id}`); - IcebergDatalakeService.validateCatalogWarehousePair({ + const updatedConfig = { ...instances[idx], ...data, - }); + }; + IcebergDatalakeService.validateCatalogWarehousePair(updatedConfig); + IcebergDatalakeService.validateCatalogAuthentication(updatedConfig); // Handle access token update if (data.accessToken) { @@ -608,9 +732,21 @@ export class IcebergDatalakeService { instances[idx].catalogAccessTokenKey = key; } - // Strip the raw accessToken before persisting - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { accessToken: _accessTokenUpdate, ...rest } = data; + if (data.oauthClientSecret) { + const key = + instances[idx].oauthClientSecretKey ?? `iceberg-oauth-secret-${id}`; + await secureStorage.setCredential(key, data.oauthClientSecret); + instances[idx].oauthClientSecretKey = key; + } + + // Strip raw secrets before persisting + /* eslint-disable @typescript-eslint/no-unused-vars */ + const { + accessToken: _accessTokenUpdate, + oauthClientSecret: _oauthClientSecretUpdate, + ...rest + } = data; + /* eslint-enable @typescript-eslint/no-unused-vars */ instances[idx] = { ...instances[idx], @@ -645,6 +781,17 @@ export class IcebergDatalakeService { ); } } + if (instance.oauthClientSecretKey) { + try { + await secureStorage.deleteCredential(instance.oauthClientSecretKey); + } catch (keyError) { + // eslint-disable-next-line no-console + console.error( + '[IcebergDatalakeService] OAuth secret delete error:', + keyError, + ); + } + } const updated = instances.filter((i) => i.id !== id); await IcebergDatalakeService.writeInstances(updated); @@ -676,6 +823,14 @@ export class IcebergDatalakeService { }, false, ); + IcebergDatalakeService.validateCatalogAuthentication({ + catalogType: params.catalogType, + catalogAuthMode: params.authMode, + accessToken: params.accessToken, + oauthClientId: params.oauthClientId, + oauthClientSecret: params.oauthClientSecret, + oauthServerUri: params.oauthServerUri, + }); if (params.endpoint) props.uri = params.endpoint; if (params.catalogName) props.warehouse = params.catalogName; @@ -709,6 +864,19 @@ export class IcebergDatalakeService { // eslint-disable-next-line dot-notation env['ICEBERG_ACCESS_TOKEN'] = params.accessToken; } + if (params.authMode === 'oauth-client-credentials') { + if (!params.oauthClientId || !params.oauthClientSecret) { + throw new Error('ICEBERG_OAUTH_CLIENT_CREDENTIALS_REQUIRED'); + } + props.credential = '__ENV:ICEBERG_OAUTH_CREDENTIAL'; + env.ICEBERG_OAUTH_CREDENTIAL = `${params.oauthClientId}:${params.oauthClientSecret}`; + if (params.oauthServerUri) { + props['oauth2-server-uri'] = params.oauthServerUri; + } + if (params.oauthScope) props.scope = params.oauthScope; + delete props.token; + delete env.ICEBERG_ACCESS_TOKEN; + } break; default: throw new Error(`ICEBERG_CATALOG_NOT_ENABLED: ${params.catalogType}`); diff --git a/src/renderer/components/dataLake/IcebergConnectionWizard.tsx b/src/renderer/components/dataLake/IcebergConnectionWizard.tsx index 717dab6f..5f00ba22 100644 --- a/src/renderer/components/dataLake/IcebergConnectionWizard.tsx +++ b/src/renderer/components/dataLake/IcebergConnectionWizard.tsx @@ -66,7 +66,12 @@ export interface IcebergWizardData { endpoint?: string; catalogName?: string; databaseConnectionId?: string; + authMode?: 'none' | 'token' | 'oauth-client-credentials'; accessToken?: string; + oauthClientId?: string; + oauthClientSecret?: string; + oauthServerUri?: string; + oauthScope?: string; polarisConnectionId?: string; polarisBucket?: string; polarisPrefix?: string; @@ -115,8 +120,13 @@ function buildInitialData(initial?: IcebergInstanceConfig): IcebergWizardData { endpoint: initial.endpoint, catalogName: initial.catalogName, databaseConnectionId: initial.databaseConnectionId, + authMode: initial.catalogAuthMode ?? 'none', // Token is masked — never pre-filled; placeholder shown instead accessToken: undefined, + oauthClientId: initial.oauthClientId, + oauthClientSecret: undefined, + oauthServerUri: initial.oauthServerUri, + oauthScope: initial.oauthScope, polarisConnectionId: initial.catalogConnectionId, polarisBucket: initial.catalogBucket, polarisPrefix: initial.catalogPrefix, @@ -137,7 +147,11 @@ function buildInitialData(initial?: IcebergInstanceConfig): IcebergWizardData { // ─── Validation helpers ─────────────────────────────────────────────────────── -function validateStep(step: number, data: IcebergWizardData): string | null { +function validateStep( + step: number, + data: IcebergWizardData, + hasExistingOAuthSecret = false, +): string | null { if (step === 0) { if (!data.basics.name.trim()) return 'Instance name is required.'; if (data.basics.name.trim().length > 80) @@ -163,6 +177,25 @@ function validateStep(step: number, data: IcebergWizardData): string | null { if (!data.catalog.endpoint) return 'REST endpoint is required.'; if (!data.catalog.catalogName) return 'Catalog name / warehouse is required.'; + if ( + data.catalog.authMode === 'oauth-client-credentials' && + !data.catalog.oauthClientId + ) { + return 'OAuth Client ID is required.'; + } + if ( + data.catalog.authMode === 'oauth-client-credentials' && + !data.catalog.oauthClientSecret && + !hasExistingOAuthSecret + ) { + return 'OAuth Client Secret is required.'; + } + if ( + data.catalog.authMode === 'oauth-client-credentials' && + !data.catalog.oauthServerUri + ) { + return 'OAuth Token Endpoint is required.'; + } } } if (step === 2) { @@ -202,6 +235,7 @@ export const IcebergConnectionWizard: React.FC< ); const [stepError, setStepError] = useState(null); const [showToken, setShowToken] = useState(false); + const [showClientSecret, setShowClientSecret] = useState(false); const [catalogTestResult, setCatalogTestResult] = useState<{ success: boolean; message: string; @@ -244,7 +278,11 @@ export const IcebergConnectionWizard: React.FC< // ── Step navigation ───────────────────────────────────────────────────── const handleNext = () => { - const err = validateStep(activeStep, data); + const err = validateStep( + activeStep, + data, + mode === 'edit' && !!initialData?.oauthClientSecretKey, + ); if (err) { setStepError(err); return; @@ -261,7 +299,11 @@ export const IcebergConnectionWizard: React.FC< }; const handleFinish = async () => { - const err = validateStep(3, data); + const err = validateStep( + 3, + data, + mode === 'edit' && !!initialData?.oauthClientSecretKey, + ); if (err) { setStepError(err); return; @@ -321,6 +363,11 @@ export const IcebergConnectionWizard: React.FC< catalogName: data.catalog.catalogName, connectionId: data.catalog.polarisConnectionId, accessToken: data.catalog.accessToken, + authMode: data.catalog.authMode, + oauthClientId: data.catalog.oauthClientId, + oauthClientSecret: data.catalog.oauthClientSecret, + oauthServerUri: data.catalog.oauthServerUri, + oauthScope: data.catalog.oauthScope, databaseConnectionId: data.catalog.databaseConnectionId, storageType: data.storage.storageType, }); @@ -416,7 +463,7 @@ export const IcebergConnectionWizard: React.FC< disabled={!capability.enabled} > {capability.label} - {!capability.enabled ? ' — Coming next' : ''} + {!capability.enabled ? ' — Coming Next' : ''} ))} @@ -528,7 +575,30 @@ export const IcebergConnectionWizard: React.FC< required helperText="The catalog name or warehouse identifier" /> - + + Authentication + + + {data.catalog.authMode === 'token' && ( - + )} + {data.catalog.authMode === 'oauth-client-credentials' && ( + <> + + patchCatalog({ oauthClientId: event.target.value }) + } + fullWidth + required + /> + + patchCatalog({ oauthClientSecret: event.target.value }) + } + fullWidth + required={ + !(mode === 'edit' && initialData?.oauthClientSecretKey) + } + helperText={ + mode === 'edit' && initialData?.oauthClientSecretKey + ? 'Leave blank to keep the existing secret.' + : 'Stored securely in the system keychain.' + } + InputProps={{ + endAdornment: ( + setShowClientSecret((value) => !value)} + > + {showClientSecret ? : } + + ), + }} + /> + + patchCatalog({ oauthServerUri: event.target.value }) + } + fullWidth + required + helperText="Explicit endpoint avoids deprecated REST catalog URL inference." + /> + + patchCatalog({ oauthScope: event.target.value }) + } + fullWidth + /> + + )} )} diff --git a/src/renderer/screens/dataLake/index.tsx b/src/renderer/screens/dataLake/index.tsx index c1497651..f71d53fa 100644 --- a/src/renderer/screens/dataLake/index.tsx +++ b/src/renderer/screens/dataLake/index.tsx @@ -241,7 +241,12 @@ const DataLake: React.FC = () => { endpoint: wizardData.catalog.endpoint, catalogName: wizardData.catalog.catalogName, databaseConnectionId: wizardData.catalog.databaseConnectionId, + catalogAuthMode: wizardData.catalog.authMode, accessToken: wizardData.catalog.accessToken, + oauthClientId: wizardData.catalog.oauthClientId, + oauthClientSecret: wizardData.catalog.oauthClientSecret, + oauthServerUri: wizardData.catalog.oauthServerUri, + oauthScope: wizardData.catalog.oauthScope, catalogConnectionId: wizardData.catalog.polarisConnectionId, catalogBucket: wizardData.catalog.polarisBucket, catalogPrefix: wizardData.catalog.polarisPrefix, @@ -274,10 +279,17 @@ const DataLake: React.FC = () => { endpoint: wizardData.catalog.endpoint, catalogName: wizardData.catalog.catalogName, databaseConnectionId: wizardData.catalog.databaseConnectionId, + catalogAuthMode: wizardData.catalog.authMode, // Only send token if the user typed a new one; empty = preserve existing ...(wizardData.catalog.accessToken ? { accessToken: wizardData.catalog.accessToken } : {}), + ...(wizardData.catalog.oauthClientSecret + ? { oauthClientSecret: wizardData.catalog.oauthClientSecret } + : {}), + oauthClientId: wizardData.catalog.oauthClientId, + oauthServerUri: wizardData.catalog.oauthServerUri, + oauthScope: wizardData.catalog.oauthScope, catalogConnectionId: wizardData.catalog.polarisConnectionId, catalogBucket: wizardData.catalog.polarisBucket, catalogPrefix: wizardData.catalog.polarisPrefix, diff --git a/src/types/iceberg.ts b/src/types/iceberg.ts index 1ff45d76..04146683 100644 --- a/src/types/iceberg.ts +++ b/src/types/iceberg.ts @@ -60,6 +60,11 @@ export interface IcebergInstanceConfig { catalogPath?: string; // local testing: path to the SQLite catalog database endpoint?: string; // REST: Polaris/Lakekeeper endpoint URL catalogName?: string; // REST: catalog name or warehouse + catalogAuthMode?: IcebergCatalogAuthMode; + oauthClientId?: string; + oauthClientSecretKey?: string; // keytar key: "iceberg-oauth-secret-{id}" + oauthServerUri?: string; + oauthScope?: string; databaseConnectionId?: string; // Existing PostgreSQL/Neon connection catalogAccessTokenKey?: string; // keytar key: "iceberg-catalog-token-{id}" catalogConnectionId?: string; // Cloud Explorer connectionId for vended credentials @@ -96,6 +101,7 @@ export type CreateIcebergInstanceDTO = Omit< 'id' | 'createdAt' | 'updatedAt' > & { accessToken?: string; // raw token — service stores in keytar, strips before saving + oauthClientSecret?: string; // raw secret — service stores in keytar, strips before saving }; export type UpdateIcebergInstanceDTO = Partial; @@ -153,6 +159,11 @@ export interface IcebergTestCatalogParams { catalogName?: string; connectionId?: string; // resolves credentials from Cloud Explorer accessToken?: string; // raw token (not stored yet at test time) + authMode?: IcebergCatalogAuthMode; + oauthClientId?: string; + oauthClientSecret?: string; // raw secret (not stored yet at test time) + oauthServerUri?: string; + oauthScope?: string; databaseConnectionId?: string; storageType?: IcebergStorageType; } diff --git a/tests/python/test_iceberg_rest_catalog.py b/tests/python/test_iceberg_rest_catalog.py new file mode 100644 index 00000000..c7979fd8 --- /dev/null +++ b/tests/python/test_iceberg_rest_catalog.py @@ -0,0 +1,115 @@ +"""Opt-in persisted-table acceptance test for REST/Polaris catalogs. + +Run with ICEBERG_REST_ACCEPTANCE=1 and the catalog environment variables below. +Secrets are read only from the environment and never written to test output. +""" + +import os +import unittest + +import pyarrow as pa +from pyiceberg.catalog import load_catalog +from pyiceberg.schema import Schema +from pyiceberg.types import LongType, NestedField, StringType + + +@unittest.skipUnless( + os.environ.get("ICEBERG_REST_ACCEPTANCE") == "1", + "REST catalog acceptance is opt-in", +) +class RestCatalogAcceptanceTest(unittest.TestCase): + namespace = ("dbt_studio_acceptance",) + table_identifier = ("dbt_studio_acceptance", "customers") + + def catalog_properties(self) -> dict[str, str]: + properties = { + "type": "rest", + "uri": os.environ["ICEBERG_REST_URI"], + "warehouse": os.environ["ICEBERG_REST_WAREHOUSE"], + "credential": os.environ["ICEBERG_REST_CREDENTIAL"], + } + if oauth_uri := os.environ.get("ICEBERG_REST_OAUTH_URI"): + properties["oauth2-server-uri"] = oauth_uri + if scope := os.environ.get("ICEBERG_REST_SCOPE"): + properties["scope"] = scope + if delegation := os.environ.get("ICEBERG_REST_ACCESS_DELEGATION"): + properties["header.X-Iceberg-Access-Delegation"] = delegation + if s3_endpoint := os.environ.get("ICEBERG_S3_ENDPOINT"): + properties["s3.endpoint"] = s3_endpoint + properties["s3.region"] = os.environ.get("ICEBERG_S3_REGION", "us-east-1") + properties["s3.access-key-id"] = os.environ["ICEBERG_S3_ACCESS_KEY_ID"] + properties["s3.secret-access-key"] = os.environ[ + "ICEBERG_S3_SECRET_ACCESS_KEY" + ] + properties["s3.force-virtual-addressing"] = "false" + return properties + + def test_create_close_reload_and_inspect_three_rows(self) -> None: + catalog = load_catalog("acceptance", **self.catalog_properties()) + if self.namespace not in catalog.list_namespaces(): + catalog.create_namespace(self.namespace) + + if self.table_identifier not in catalog.list_tables(self.namespace): + table = catalog.create_table( + self.table_identifier, + schema=Schema( + NestedField(1, "id", LongType(), required=False), + NestedField(2, "name", StringType(), required=False), + ), + ) + else: + table = catalog.load_table(self.table_identifier) + + existing_rows = table.scan().to_arrow().to_pylist() + if not existing_rows: + table.append( + pa.Table.from_arrays( + [ + pa.array([1, 2, 3], type=pa.int64()), + pa.array(["Ada", "Linus", "Grace"]), + ], + schema=pa.schema( + [ + pa.field("id", pa.int64(), nullable=False), + pa.field("name", pa.string(), nullable=True), + ] + ), + ) + ) + else: + self.assertEqual( + existing_rows, + [ + {"id": 1, "name": "Ada"}, + {"id": 2, "name": "Linus"}, + {"id": 3, "name": "Grace"}, + ], + ) + catalog.close() + + reloaded = load_catalog("acceptance", **self.catalog_properties()) + self.assertIn(self.namespace, reloaded.list_namespaces()) + self.assertIn( + self.table_identifier, + reloaded.list_tables(self.namespace), + ) + + table = reloaded.load_table(self.table_identifier) + rows = table.scan().to_arrow().to_pylist() + self.assertEqual( + rows, + [ + {"id": 1, "name": "Ada"}, + {"id": 2, "name": "Linus"}, + {"id": 3, "name": "Grace"}, + ], + ) + self.assertEqual([field.name for field in table.schema().fields], ["id", "name"]) + self.assertTrue(list(table.snapshots())) + self.assertIsInstance(table.properties, dict) + self.assertIsNotNone(next(iter(table.scan(limit=1).plan_files()), None)) + reloaded.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/main/services/icebergDatalake.service.test.ts b/tests/unit/main/services/icebergDatalake.service.test.ts new file mode 100644 index 00000000..69a7b79f --- /dev/null +++ b/tests/unit/main/services/icebergDatalake.service.test.ts @@ -0,0 +1,119 @@ +import { IcebergDatalakeService } from '../../../../src/main/services/icebergDatalake.service'; +import secureStorage from '../../../../src/main/services/secureStorage.service'; +import { + loadDatabaseFile, + updateDatabase, +} from '../../../../src/main/utils/fileHelper'; + +jest.mock('../../../../src/main/utils/fileHelper', () => ({ + loadDatabaseFile: jest.fn(), + updateDatabase: jest.fn(), +})); + +jest.mock('../../../../src/main/services/secureStorage.service', () => ({ + __esModule: true, + default: { + setCredential: jest.fn(), + getCredential: jest.fn(), + deleteCredential: jest.fn(), + }, +})); + +jest.mock('../../../../src/main/services/settings.service', () => ({ + __esModule: true, + default: { loadSettings: jest.fn() }, +})); + +const mockedLoadDatabase = loadDatabaseFile as jest.Mock; +const mockedUpdateDatabase = updateDatabase as jest.Mock; +const mockedSecureStorage = secureStorage as jest.Mocked; + +describe('IcebergDatalakeService compatibility and secret persistence', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockedLoadDatabase.mockResolvedValue({ icebergInstances: [] }); + mockedUpdateDatabase.mockResolvedValue(undefined); + mockedSecureStorage.setCredential.mockResolvedValue(undefined); + }); + + it('keeps Hadoop disabled while exposing modern catalog capabilities', () => { + const capabilities = IcebergDatalakeService.getCapabilities(); + + expect( + capabilities.catalogs.find(({ type }) => type === 'hadoop'), + ).toMatchObject({ enabled: false }); + expect( + capabilities.catalogs.find(({ type }) => type === 'polaris'), + ).toMatchObject({ + enabled: true, + authModes: expect.arrayContaining(['oauth-client-credentials']), + }); + }); + + it('rejects invalid OAuth configuration before bridge execution', () => { + const validate = (IcebergDatalakeService as any) + .validateCatalogAuthentication as (config: unknown) => void; + + expect(() => + validate({ + catalogType: 'polaris', + catalogAuthMode: 'oauth-client-credentials', + oauthClientId: 'root', + oauthClientSecret: 'secret', + }), + ).toThrow('ICEBERG_OAUTH_SERVER_URI_REQUIRED'); + expect(() => + validate({ + catalogType: 'polaris', + catalogAuthMode: 'oauth-client-credentials', + oauthClientId: 'invalid:id', + oauthClientSecret: 'secret', + oauthServerUri: 'http://localhost/oauth/tokens', + }), + ).toThrow('ICEBERG_OAUTH_CLIENT_ID_INVALID'); + }); + + it('stores the OAuth secret in keytar and excludes it from database persistence', async () => { + const created = await IcebergDatalakeService.createInstance({ + name: 'polaris-test', + catalogType: 'polaris', + endpoint: 'http://localhost:8181/api/catalog', + catalogName: 'quickstart_catalog', + catalogAuthMode: 'oauth-client-credentials', + oauthClientId: 'root', + oauthClientSecret: 'top-secret', + oauthServerUri: 'http://localhost:8181/api/catalog/v1/oauth/tokens', + oauthScope: 'PRINCIPAL_ROLE:ALL', + storageType: 'server-managed', + }); + + expect(mockedSecureStorage.setCredential).toHaveBeenCalledWith( + `iceberg-oauth-secret-${created.id}`, + 'top-secret', + ); + const persisted = mockedUpdateDatabase.mock.calls[0][1][0]; + expect(persisted.oauthClientSecret).toBeUndefined(); + expect(JSON.stringify(persisted)).not.toContain('top-secret'); + expect(persisted.oauthClientSecretKey).toBe( + `iceberg-oauth-secret-${created.id}`, + ); + }); + + it('redacts OAuth and database secrets from bridge errors', () => { + const redact = (IcebergDatalakeService as any).redactBridgeSecrets as ( + message: string, + env: Record, + ) => string; + const message = redact( + 'OAuth top-secret failed; URI postgresql+psycopg2://user:db-secret@host/db', + { + ICEBERG_OAUTH_CREDENTIAL: 'client-id:top-secret', + ICEBERG_SQL_CATALOG_URI: 'postgresql+psycopg2://user:db-secret@host/db', + }, + ); + + expect(message).not.toContain('top-secret'); + expect(message).not.toContain('db-secret'); + expect(message).toContain('[REDACTED]'); + }); +}); From 86e27b719e284d754a01d23400a04559c4438404 Mon Sep 17 00:00:00 2001 From: Nuri Lacka Date: Sun, 9 Aug 2026 19:44:50 +0200 Subject: [PATCH 05/12] feat(iceberg): add Project Nessie catalog support - enable Project Nessie as a PyIceberg REST catalog - support Nessie references and optional named warehouses - validate Iceberg REST endpoints and reject native API v2 URLs - configure server-managed storage with remote request signing - add Nessie catalog fields to create, edit, test, and persistence flows - present Nessie warehouse configuration in the management overview - preserve catalog-provided FileIO and object-store properties - add focused tests for capabilities, URI construction, and validation - extend persisted-table acceptance for unauthenticated REST catalogs - verify Nessie with MinIO through create, close, reload, and table inspection --- src/main/services/icebergDatalake.service.ts | 61 +++++- .../dataLake/IcebergConnectionWizard.tsx | 195 +++++++++++++----- .../dataLake/iceberg/IcebergDetail.tsx | 37 +++- src/renderer/screens/dataLake/index.tsx | 4 + src/types/iceberg.ts | 10 +- tests/python/test_iceberg_rest_catalog.py | 6 +- .../services/icebergDatalake.service.test.ts | 68 ++++++ 7 files changed, 318 insertions(+), 63 deletions(-) diff --git a/src/main/services/icebergDatalake.service.ts b/src/main/services/icebergDatalake.service.ts index 10e02e46..ebaf5fd2 100644 --- a/src/main/services/icebergDatalake.service.ts +++ b/src/main/services/icebergDatalake.service.ts @@ -84,12 +84,20 @@ export class IcebergDatalakeService { authModes: ['none', 'token', 'oauth-client-credentials'], allowedStorageTypes: ['server-managed'], }, + { + type: 'nessie', + label: 'Project Nessie', + pyicebergType: 'rest', + enabled: true, + requiredFields: ['endpoint', 'nessieReference'], + authModes: ['none', 'token', 'oauth-client-credentials'], + allowedStorageTypes: ['server-managed'], + }, ...( [ ['hive', 'Hive Metastore', 'hive'], ['hadoop', 'Hadoop Catalog', 'custom'], ['glue', 'AWS Glue', 'glue'], - ['nessie', 'Project Nessie', 'rest'], ] as const ).map(([type, label, pyicebergType]) => ({ type, @@ -285,9 +293,15 @@ export class IcebergDatalakeService { case 'rest': case 'polaris': + case 'nessie': props.type = 'rest'; - if (instance.endpoint) props.uri = instance.endpoint; - if (instance.catalogName) props.warehouse = instance.catalogName; + if (instance.catalogType === 'nessie') { + props.uri = IcebergDatalakeService.buildNessieRestUri(instance); + props['header.X-Iceberg-Access-Delegation'] = 'remote-signing'; + } else { + if (instance.endpoint) props.uri = instance.endpoint; + if (instance.catalogName) props.warehouse = instance.catalogName; + } // Access token via env var if (instance.catalogAccessTokenKey) { try { @@ -341,6 +355,36 @@ export class IcebergDatalakeService { }; } + private static buildNessieRestUri( + config: Pick< + IcebergInstanceConfig, + 'endpoint' | 'nessieReference' | 'nessieWarehouse' + >, + ): string { + const endpoint = config.endpoint?.trim().replace(/\/+$/, ''); + const reference = config.nessieReference?.trim(); + if (!endpoint) throw new Error('ICEBERG_REQUIRED_FIELD: endpoint'); + if (!reference) { + throw new Error('ICEBERG_REQUIRED_FIELD: nessieReference'); + } + let parsed: URL; + try { + parsed = new URL(endpoint); + } catch { + throw new Error('ICEBERG_NESSIE_ENDPOINT_INVALID'); + } + if (!['http:', 'https:'].includes(parsed.protocol)) { + throw new Error('ICEBERG_NESSIE_ENDPOINT_INVALID'); + } + if (!parsed.pathname.endsWith('/iceberg')) { + throw new Error('ICEBERG_NESSIE_ICEBERG_REST_ENDPOINT_REQUIRED'); + } + const warehouse = config.nessieWarehouse?.trim(); + return `${endpoint}/${encodeURIComponent(reference)}${ + warehouse ? `|${encodeURIComponent(warehouse)}` : '' + }`; + } + private static async buildWarehouseProperties( instance: IcebergInstanceConfig, ): Promise<{ props: Record; env: Record }> { @@ -507,6 +551,8 @@ export class IcebergDatalakeService { | 'localPath' | 'storageConnectionId' | 'storageBucket' + | 'nessieReference' + | 'nessieWarehouse' >, validateWarehouseFields = true, ): void { @@ -527,6 +573,9 @@ export class IcebergDatalakeService { if (missingField) { throw new Error(`ICEBERG_REQUIRED_FIELD: ${missingField}`); } + if (config.catalogType === 'nessie') { + IcebergDatalakeService.buildNessieRestUri(config); + } if ( validateWarehouseFields && config.storageType === 'local' && @@ -858,7 +907,13 @@ export class IcebergDatalakeService { break; case 'rest': case 'polaris': + case 'nessie': props.type = 'rest'; + if (params.catalogType === 'nessie') { + props.uri = IcebergDatalakeService.buildNessieRestUri(params); + props['header.X-Iceberg-Access-Delegation'] = 'remote-signing'; + delete props.warehouse; + } if (params.accessToken) { props.token = '__ENV:ICEBERG_ACCESS_TOKEN'; // eslint-disable-next-line dot-notation diff --git a/src/renderer/components/dataLake/IcebergConnectionWizard.tsx b/src/renderer/components/dataLake/IcebergConnectionWizard.tsx index 5f00ba22..596c8c50 100644 --- a/src/renderer/components/dataLake/IcebergConnectionWizard.tsx +++ b/src/renderer/components/dataLake/IcebergConnectionWizard.tsx @@ -72,6 +72,8 @@ export interface IcebergWizardData { oauthClientSecret?: string; oauthServerUri?: string; oauthScope?: string; + nessieReference?: string; + nessieWarehouse?: string; polarisConnectionId?: string; polarisBucket?: string; polarisPrefix?: string; @@ -127,13 +129,17 @@ function buildInitialData(initial?: IcebergInstanceConfig): IcebergWizardData { oauthClientSecret: undefined, oauthServerUri: initial.oauthServerUri, oauthScope: initial.oauthScope, + nessieReference: initial.nessieReference, + nessieWarehouse: initial.nessieWarehouse, polarisConnectionId: initial.catalogConnectionId, polarisBucket: initial.catalogBucket, polarisPrefix: initial.catalogPrefix, }, storage: { storageType: - initial.catalogType === 'rest' || initial.catalogType === 'polaris' + initial.catalogType === 'rest' || + initial.catalogType === 'polaris' || + initial.catalogType === 'nessie' ? 'server-managed' : initial.storageType, localPath: initial.localPath, @@ -172,11 +178,18 @@ function validateStep( } if ( data.catalog.catalogType === 'rest' || - data.catalog.catalogType === 'polaris' + data.catalog.catalogType === 'polaris' || + data.catalog.catalogType === 'nessie' ) { if (!data.catalog.endpoint) return 'REST endpoint is required.'; - if (!data.catalog.catalogName) + if (data.catalog.catalogType !== 'nessie' && !data.catalog.catalogName) return 'Catalog name / warehouse is required.'; + if ( + data.catalog.catalogType === 'nessie' && + !data.catalog.nessieReference + ) { + return 'Nessie reference is required.'; + } if ( data.catalog.authMode === 'oauth-client-credentials' && !data.catalog.oauthClientId @@ -201,7 +214,8 @@ function validateStep( if (step === 2) { if ( data.catalog.catalogType === 'rest' || - data.catalog.catalogType === 'polaris' + data.catalog.catalogType === 'polaris' || + data.catalog.catalogType === 'nessie' ) { // REST catalogs manage warehouse storage server-side; vended creds optional return null; @@ -368,6 +382,8 @@ export const IcebergConnectionWizard: React.FC< oauthClientSecret: data.catalog.oauthClientSecret, oauthServerUri: data.catalog.oauthServerUri, oauthScope: data.catalog.oauthScope, + nessieReference: data.catalog.nessieReference, + nessieWarehouse: data.catalog.nessieWarehouse, databaseConnectionId: data.catalog.databaseConnectionId, storageType: data.storage.storageType, }); @@ -440,6 +456,8 @@ export const IcebergConnectionWizard: React.FC< endpoint: undefined, catalogName: undefined, databaseConnectionId: undefined, + nessieReference: catalogType === 'nessie' ? 'main' : undefined, + nessieWarehouse: undefined, accessToken: undefined, polarisConnectionId: undefined, polarisBucket: undefined, @@ -555,26 +573,65 @@ export const IcebergConnectionWizard: React.FC< )} {(data.catalog.catalogType === 'rest' || - data.catalog.catalogType === 'polaris') && ( + data.catalog.catalogType === 'polaris' || + data.catalog.catalogType === 'nessie') && ( <> patchCatalog({ endpoint: e.target.value })} fullWidth required - helperText="The REST catalog server URL" - /> - patchCatalog({ catalogName: e.target.value })} - fullWidth - required - helperText="The catalog name or warehouse identifier" + helperText={ + data.catalog.catalogType === 'nessie' + ? 'Use the Iceberg REST endpoint ending in /iceberg, not the Nessie /api/v2 endpoint.' + : 'The REST catalog server URL' + } /> + {data.catalog.catalogType === 'nessie' ? ( + <> + + patchCatalog({ nessieReference: event.target.value }) + } + fullWidth + required + helperText="Branch or tag to read and write through Iceberg REST" + /> + + patchCatalog({ nessieWarehouse: event.target.value }) + } + fullWidth + helperText="Leave blank to use the Nessie server's default warehouse" + /> + + ) : ( + patchCatalog({ catalogName: e.target.value })} + fullWidth + required + helperText="The catalog name or warehouse identifier" + /> + )} Authentication Local Filesystem - - Cloud Storage (Cloud Explorer connection) - + {selectedCatalogCapability?.allowedStorageTypes.includes( + 'cloud', + ) && ( + + Cloud Storage (Cloud Explorer connection) + + )} @@ -972,6 +1031,22 @@ export const IcebergConnectionWizard: React.FC< )} + {data.catalog.catalogType === 'hive' && ( + <> + + + + + + + + )} {(data.catalog.catalogType === 'rest' || data.catalog.catalogType === 'polaris' || data.catalog.catalogType === 'nessie') && ( diff --git a/src/renderer/components/dataLake/iceberg/IcebergDetail.tsx b/src/renderer/components/dataLake/iceberg/IcebergDetail.tsx index e41b9791..ba1b8543 100644 --- a/src/renderer/components/dataLake/iceberg/IcebergDetail.tsx +++ b/src/renderer/components/dataLake/iceberg/IcebergDetail.tsx @@ -902,6 +902,16 @@ export const IcebergDetail: React.FC = ({ ); }; + let catalogIdentityLabel = 'Catalog Name'; + let catalogIdentityValue = instance.catalogName ?? 'Local catalog'; + if (instance.catalogType === 'nessie') { + catalogIdentityLabel = 'Reference'; + catalogIdentityValue = instance.nessieReference ?? 'main'; + } else if (instance.catalogType === 'hive') { + catalogIdentityLabel = 'Metastore URI'; + catalogIdentityValue = instance.hiveUri ?? '—'; + } + return ( @@ -1063,16 +1073,8 @@ export const IcebergDetail: React.FC = ({ {instance.catalogType === 'nessie' && ( @@ -1088,6 +1090,17 @@ export const IcebergDetail: React.FC = ({ /> )} + {instance.catalogType === 'hive' && ( + + + + + + + )} diff --git a/src/renderer/screens/dataLake/index.tsx b/src/renderer/screens/dataLake/index.tsx index fe816fa9..04c3fddd 100644 --- a/src/renderer/screens/dataLake/index.tsx +++ b/src/renderer/screens/dataLake/index.tsx @@ -249,6 +249,8 @@ const DataLake: React.FC = () => { oauthScope: wizardData.catalog.oauthScope, nessieReference: wizardData.catalog.nessieReference, nessieWarehouse: wizardData.catalog.nessieWarehouse, + hiveUri: wizardData.catalog.hiveUri, + hiveUgi: wizardData.catalog.hiveUgi, catalogConnectionId: wizardData.catalog.polarisConnectionId, catalogBucket: wizardData.catalog.polarisBucket, catalogPrefix: wizardData.catalog.polarisPrefix, @@ -294,6 +296,8 @@ const DataLake: React.FC = () => { oauthScope: wizardData.catalog.oauthScope, nessieReference: wizardData.catalog.nessieReference, nessieWarehouse: wizardData.catalog.nessieWarehouse, + hiveUri: wizardData.catalog.hiveUri, + hiveUgi: wizardData.catalog.hiveUgi, catalogConnectionId: wizardData.catalog.polarisConnectionId, catalogBucket: wizardData.catalog.polarisBucket, catalogPrefix: wizardData.catalog.polarisPrefix, diff --git a/src/types/iceberg.ts b/src/types/iceberg.ts index 9cbe2c71..c979d3fd 100644 --- a/src/types/iceberg.ts +++ b/src/types/iceberg.ts @@ -44,6 +44,7 @@ export interface IcebergCatalogCapability { | 'endpoint' | 'catalogName' | 'databaseConnectionId' + | 'hiveUri' | 'nessieReference' >; authModes: IcebergCatalogAuthMode[]; @@ -71,6 +72,8 @@ export interface IcebergInstanceConfig { oauthScope?: string; nessieReference?: string; // Nessie branch or tag, usually "main" nessieWarehouse?: string; // optional named Nessie warehouse + hiveUri?: string; // Hive Metastore Thrift URI, e.g. thrift://localhost:9083 + hiveUgi?: string; // optional Hive user:group identity for non-Kerberos HMS databaseConnectionId?: string; // Existing PostgreSQL/Neon connection catalogAccessTokenKey?: string; // keytar key: "iceberg-catalog-token-{id}" catalogConnectionId?: string; // Cloud Explorer connectionId for vended credentials @@ -172,6 +175,8 @@ export interface IcebergTestCatalogParams { oauthScope?: string; nessieReference?: string; nessieWarehouse?: string; + hiveUri?: string; + hiveUgi?: string; databaseConnectionId?: string; storageType?: IcebergStorageType; } diff --git a/tests/python/test_iceberg_hive_catalog.py b/tests/python/test_iceberg_hive_catalog.py new file mode 100644 index 00000000..045492da --- /dev/null +++ b/tests/python/test_iceberg_hive_catalog.py @@ -0,0 +1,78 @@ +"""Opt-in persisted-table acceptance test for the Hive Metastore catalog.""" + +import os +import unittest + +import pyarrow as pa +from pyiceberg.catalog import load_catalog +from pyiceberg.schema import Schema +from pyiceberg.types import DoubleType, LongType, NestedField, StringType + + +@unittest.skipUnless( + os.environ.get("ICEBERG_HIVE_ACCEPTANCE") == "1", + "Hive catalog acceptance is opt-in", +) +class HiveCatalogAcceptanceTest(unittest.TestCase): + namespace = ("dbt_studio_hive_acceptance",) + table_identifier = (*namespace, "sales") + + def catalog_properties(self) -> dict[str, str]: + return { + "type": "hive", + "uri": os.environ["ICEBERG_HIVE_URI"], + "warehouse": os.environ["ICEBERG_HIVE_WAREHOUSE"], + } + + def test_create_close_reload_and_inspect_three_rows(self) -> None: + properties = self.catalog_properties() + catalog = load_catalog("hive_acceptance", **properties) + if self.namespace not in catalog.list_namespaces(): + catalog.create_namespace( + self.namespace, + {"location": properties["warehouse"]}, + ) + + if self.table_identifier not in catalog.list_tables(self.namespace): + table = catalog.create_table( + self.table_identifier, + schema=Schema( + NestedField(1, "sale_id", LongType(), required=False), + NestedField(2, "product", StringType(), required=False), + NestedField(3, "amount", DoubleType(), required=False), + ), + ) + table.append( + pa.table( + { + "sale_id": pa.array([1, 2, 3], type=pa.int64()), + "product": ["Starter", "Analytics", "Enterprise"], + "amount": pa.array( + [49.0, 129.5, 399.0], + type=pa.float64(), + ), + } + ) + ) + catalog.close() + + reloaded = load_catalog("hive_acceptance_reload", **properties) + self.assertIn(self.namespace, reloaded.list_namespaces()) + self.assertIn( + self.table_identifier, + reloaded.list_tables(self.namespace), + ) + table = reloaded.load_table(self.table_identifier) + self.assertEqual(table.scan().count(), 3) + self.assertEqual( + [field.name for field in table.schema().fields], + ["sale_id", "product", "amount"], + ) + self.assertTrue(list(table.snapshots())) + self.assertIsInstance(table.properties, dict) + self.assertIsNotNone(next(iter(table.scan(limit=1).plan_files()), None)) + reloaded.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/main/services/icebergDatalake.service.test.ts b/tests/unit/main/services/icebergDatalake.service.test.ts index e5ec51ce..1eac00b0 100644 --- a/tests/unit/main/services/icebergDatalake.service.test.ts +++ b/tests/unit/main/services/icebergDatalake.service.test.ts @@ -56,6 +56,14 @@ describe('IcebergDatalakeService compatibility and secret persistence', () => { requiredFields: ['endpoint', 'nessieReference'], allowedStorageTypes: ['server-managed'], }); + expect( + capabilities.catalogs.find(({ type }) => type === 'hive'), + ).toMatchObject({ + enabled: true, + pyicebergType: 'hive', + requiredFields: ['hiveUri'], + allowedStorageTypes: ['local'], + }); }); it('rejects invalid OAuth configuration before bridge execution', () => { @@ -184,4 +192,68 @@ describe('IcebergDatalakeService compatibility and secret persistence', () => { env: {}, }); }); + + it('validates Hive Thrift URIs and optional UGI identity', () => { + const validate = (IcebergDatalakeService as any) + .validateCatalogWarehousePair as (config: unknown) => void; + + expect(() => + validate({ + catalogType: 'hive', + hiveUri: 'http://localhost:9083', + storageType: 'local', + localPath: '/tmp/hive-warehouse', + }), + ).toThrow('ICEBERG_HIVE_URI_INVALID'); + expect(() => + validate({ + catalogType: 'hive', + hiveUri: 'thrift://localhost:9083', + hiveUgi: 'invalid', + storageType: 'local', + localPath: '/tmp/hive-warehouse', + }), + ).toThrow('ICEBERG_HIVE_UGI_INVALID'); + }); + + it('builds native Hive catalog properties independently from storage', async () => { + const buildProperties = (IcebergDatalakeService as any) + .buildCatalogProperties as (config: unknown) => Promise<{ + props: Record; + env: Record; + }>; + + const result = await buildProperties({ + catalogType: 'hive', + hiveUri: 'thrift://localhost:9083', + hiveUgi: 'dbt:analytics', + storageType: 'local', + localPath: '/tmp/hive-warehouse', + }); + + expect(result).toEqual({ + props: { + type: 'hive', + uri: 'thrift://localhost:9083', + ugi: 'dbt:analytics', + warehouse: 'file:///tmp/hive-warehouse', + }, + env: {}, + }); + }); + + it('rejects unverified Hive cloud warehouses before Python executes', () => { + const validate = (IcebergDatalakeService as any) + .validateCatalogWarehousePair as (config: unknown) => void; + + expect(() => + validate({ + catalogType: 'hive', + hiveUri: 'thrift://localhost:9083', + storageType: 'cloud', + storageConnectionId: 'minio', + storageBucket: 'iceberg-hive', + }), + ).toThrow('ICEBERG_WAREHOUSE_NOT_ALLOWED: hive/cloud'); + }); }); From ea4fc8eaa5c5c4ce31d50188f06636afe2bc7fa9 Mon Sep 17 00:00:00 2001 From: Nuri Lacka Date: Mon, 10 Aug 2026 18:31:14 +0200 Subject: [PATCH 07/12] feat(iceberg): add Lakekeeper acceptance and managed catalog placeholders - add disabled Google BigLake, Microsoft OneLake, and Databricks Unity Catalog options - keep all six managed catalogs marked as Coming Next - prevent disabled catalogs from exposing forms, credentials, tests, or backend execution - configure Lakekeeper as an Iceberg REST catalog with server-managed storage - request remote signing for Lakekeeper warehouse access - verify Lakekeeper and MinIO create, close, reload, and inspection workflows - cover namespace and table listing, schema, snapshots, properties, preview, and scan planning --- src/main/services/icebergDatalake.service.ts | 25 ++++- .../dataLake/IcebergConnectionWizard.tsx | 7 ++ .../dataLake/IcebergInstanceListItem.tsx | 7 +- src/types/iceberg.ts | 14 +-- .../services/icebergDatalake.service.test.ts | 106 +++++++++++++++++- 5 files changed, 145 insertions(+), 14 deletions(-) diff --git a/src/main/services/icebergDatalake.service.ts b/src/main/services/icebergDatalake.service.ts index e61d8d8d..a32a3174 100644 --- a/src/main/services/icebergDatalake.service.ts +++ b/src/main/services/icebergDatalake.service.ts @@ -84,6 +84,15 @@ export class IcebergDatalakeService { authModes: ['none', 'token', 'oauth-client-credentials'], allowedStorageTypes: ['server-managed'], }, + { + type: 'lakekeeper', + label: 'Lakekeeper', + pyicebergType: 'rest', + enabled: true, + requiredFields: ['endpoint', 'catalogName'], + authModes: ['none', 'token', 'oauth-client-credentials'], + allowedStorageTypes: ['server-managed'], + }, { type: 'nessie', label: 'Project Nessie', @@ -104,15 +113,19 @@ export class IcebergDatalakeService { }, ...( [ - ['hadoop', 'Hadoop Catalog', 'custom'], ['glue', 'AWS Glue', 'glue'], + ['biglake', 'Google BigLake', 'custom'], + ['onelake', 'Microsoft Fabric OneLake', 'custom'], + ['unity', 'Databricks Unity Catalog', 'custom'], + ['snowflake', 'Snowflake Iceberg Catalog', 'custom'], + ['cloudflare', 'Cloudflare R2 Data Catalog', 'custom'], ] as const ).map(([type, label, pyicebergType]) => ({ type, label, pyicebergType, enabled: false, - disabledReason: 'Planned for the next catalog adapter slice.', + disabledReason: 'Available by request through a GitHub issue.', requiredFields: [], authModes: ['none'] as IcebergCatalogCapability['authModes'], allowedStorageTypes: [], @@ -301,6 +314,7 @@ export class IcebergDatalakeService { case 'rest': case 'polaris': + case 'lakekeeper': case 'nessie': props.type = 'rest'; if (instance.catalogType === 'nessie') { @@ -309,6 +323,9 @@ export class IcebergDatalakeService { } else { if (instance.endpoint) props.uri = instance.endpoint; if (instance.catalogName) props.warehouse = instance.catalogName; + if (instance.catalogType === 'lakekeeper') { + props['header.X-Iceberg-Access-Delegation'] = 'remote-signing'; + } } // Access token via env var if (instance.catalogAccessTokenKey) { @@ -660,6 +677,7 @@ export class IcebergDatalakeService { mode !== 'none' && config.catalogType !== 'rest' && config.catalogType !== 'polaris' && + config.catalogType !== 'lakekeeper' && config.catalogType !== 'nessie' ) { throw new Error( @@ -958,12 +976,15 @@ export class IcebergDatalakeService { break; case 'rest': case 'polaris': + case 'lakekeeper': case 'nessie': props.type = 'rest'; if (params.catalogType === 'nessie') { props.uri = IcebergDatalakeService.buildNessieRestUri(params); props['header.X-Iceberg-Access-Delegation'] = 'remote-signing'; delete props.warehouse; + } else if (params.catalogType === 'lakekeeper') { + props['header.X-Iceberg-Access-Delegation'] = 'remote-signing'; } if (params.accessToken) { props.token = '__ENV:ICEBERG_ACCESS_TOKEN'; diff --git a/src/renderer/components/dataLake/IcebergConnectionWizard.tsx b/src/renderer/components/dataLake/IcebergConnectionWizard.tsx index 0a431f2e..351e7174 100644 --- a/src/renderer/components/dataLake/IcebergConnectionWizard.tsx +++ b/src/renderer/components/dataLake/IcebergConnectionWizard.tsx @@ -143,6 +143,7 @@ function buildInitialData(initial?: IcebergInstanceConfig): IcebergWizardData { storageType: initial.catalogType === 'rest' || initial.catalogType === 'polaris' || + initial.catalogType === 'lakekeeper' || initial.catalogType === 'nessie' ? 'server-managed' : initial.storageType, @@ -193,6 +194,7 @@ function validateStep( if ( data.catalog.catalogType === 'rest' || data.catalog.catalogType === 'polaris' || + data.catalog.catalogType === 'lakekeeper' || data.catalog.catalogType === 'nessie' ) { if (!data.catalog.endpoint) return 'REST endpoint is required.'; @@ -229,6 +231,7 @@ function validateStep( if ( data.catalog.catalogType === 'rest' || data.catalog.catalogType === 'polaris' || + data.catalog.catalogType === 'lakekeeper' || data.catalog.catalogType === 'nessie' ) { // REST catalogs manage warehouse storage server-side; vended creds optional @@ -621,6 +624,7 @@ export const IcebergConnectionWizard: React.FC< {(data.catalog.catalogType === 'rest' || data.catalog.catalogType === 'polaris' || + data.catalog.catalogType === 'lakekeeper' || data.catalog.catalogType === 'nessie') && ( <> @@ -1093,6 +1099,7 @@ export const IcebergConnectionWizard: React.FC< {data.catalog.catalogType === 'rest' || data.catalog.catalogType === 'polaris' || + data.catalog.catalogType === 'lakekeeper' || data.catalog.catalogType === 'nessie' ? ( <> diff --git a/src/renderer/components/dataLake/IcebergInstanceListItem.tsx b/src/renderer/components/dataLake/IcebergInstanceListItem.tsx index 65e0d003..cefd7df0 100644 --- a/src/renderer/components/dataLake/IcebergInstanceListItem.tsx +++ b/src/renderer/components/dataLake/IcebergInstanceListItem.tsx @@ -34,9 +34,14 @@ const catalogTypeLabel: Record = { sqlite: 'SQLite', rest: 'REST', polaris: 'Polaris', + lakekeeper: 'Lakekeeper', glue: 'Glue', + biglake: 'BigLake', + onelake: 'OneLake', + unity: 'Unity Catalog', hive: 'Hive', - hadoop: 'Hadoop', + snowflake: 'Snowflake', + cloudflare: 'Cloudflare', sql: 'SQL', nessie: 'Nessie', }; diff --git a/src/types/iceberg.ts b/src/types/iceberg.ts index c979d3fd..85bc1e1e 100644 --- a/src/types/iceberg.ts +++ b/src/types/iceberg.ts @@ -6,17 +6,17 @@ export type IcebergCatalogType = | 'sql' | 'rest' | 'polaris' + | 'lakekeeper' | 'hive' - | 'hadoop' | 'glue' + | 'biglake' + | 'onelake' + | 'unity' + | 'snowflake' + | 'cloudflare' | 'nessie'; -export type IcebergStorageType = - | 'server-managed' - | 'local' - | 'nfs' - | 'hdfs' - | 'cloud'; +export type IcebergStorageType = 'server-managed' | 'local' | 'nfs' | 'cloud'; export type IcebergCatalogAuthMode = | 'none' diff --git a/tests/unit/main/services/icebergDatalake.service.test.ts b/tests/unit/main/services/icebergDatalake.service.test.ts index 1eac00b0..88ff1009 100644 --- a/tests/unit/main/services/icebergDatalake.service.test.ts +++ b/tests/unit/main/services/icebergDatalake.service.test.ts @@ -36,18 +36,26 @@ describe('IcebergDatalakeService compatibility and secret persistence', () => { mockedSecureStorage.setCredential.mockResolvedValue(undefined); }); - it('keeps Hadoop disabled while exposing modern catalog capabilities', () => { + it('omits Hadoop while exposing modern catalog capabilities', () => { const capabilities = IcebergDatalakeService.getCapabilities(); - expect( - capabilities.catalogs.find(({ type }) => type === 'hadoop'), - ).toMatchObject({ enabled: false }); + expect(capabilities.catalogs.map(({ type }) => type)).not.toContain( + 'hadoop', + ); expect( capabilities.catalogs.find(({ type }) => type === 'polaris'), ).toMatchObject({ enabled: true, authModes: expect.arrayContaining(['oauth-client-credentials']), }); + expect( + capabilities.catalogs.find(({ type }) => type === 'lakekeeper'), + ).toMatchObject({ + enabled: true, + pyicebergType: 'rest', + requiredFields: ['endpoint', 'catalogName'], + allowedStorageTypes: ['server-managed'], + }); expect( capabilities.catalogs.find(({ type }) => type === 'nessie'), ).toMatchObject({ @@ -64,6 +72,24 @@ describe('IcebergDatalakeService compatibility and secret persistence', () => { requiredFields: ['hiveUri'], allowedStorageTypes: ['local'], }); + ( + [ + 'glue', + 'biglake', + 'onelake', + 'unity', + 'snowflake', + 'cloudflare', + ] as const + ).forEach((type) => { + expect( + capabilities.catalogs.find((catalog) => catalog.type === type), + ).toMatchObject({ + enabled: false, + disabledReason: 'Available by request through a GitHub issue.', + allowedStorageTypes: [], + }); + }); }); it('rejects invalid OAuth configuration before bridge execution', () => { @@ -115,6 +141,32 @@ describe('IcebergDatalakeService compatibility and secret persistence', () => { ); }); + it('persists Lakekeeper without client or vended storage credentials', async () => { + const created = await IcebergDatalakeService.createInstance({ + name: 'lakekeeper-test', + catalogType: 'lakekeeper', + endpoint: 'http://localhost:8181/catalog', + catalogName: 'minio-warehouse', + catalogAuthMode: 'none', + storageType: 'server-managed', + }); + + const persisted = mockedUpdateDatabase.mock.calls[0][1][0]; + expect(persisted).toMatchObject({ + id: created.id, + catalogType: 'lakekeeper', + endpoint: 'http://localhost:8181/catalog', + catalogName: 'minio-warehouse', + storageType: 'server-managed', + }); + expect(persisted.storageConnectionId).toBeUndefined(); + expect(persisted.catalogAccessTokenKey).toBeUndefined(); + expect(persisted.oauthClientSecretKey).toBeUndefined(); + expect(JSON.stringify(persisted)).not.toMatch( + /access.?key|secret.?access|session.?token/i, + ); + }); + it('redacts OAuth and database secrets from bridge errors', () => { const redact = (IcebergDatalakeService as any).redactBridgeSecrets as ( message: string, @@ -193,6 +245,52 @@ describe('IcebergDatalakeService compatibility and secret persistence', () => { }); }); + it('configures Lakekeeper REST with server-managed remote signing', async () => { + const buildProperties = (IcebergDatalakeService as any) + .buildCatalogProperties as (config: unknown) => Promise<{ + props: Record; + env: Record; + }>; + + const result = await buildProperties({ + catalogType: 'lakekeeper', + endpoint: 'http://localhost:8181/catalog', + catalogName: 'minio-warehouse', + catalogAuthMode: 'none', + storageType: 'server-managed', + }); + + expect(result).toEqual({ + props: { + type: 'rest', + uri: 'http://localhost:8181/catalog', + warehouse: 'minio-warehouse', + 'header.X-Iceberg-Access-Delegation': 'remote-signing', + }, + env: {}, + }); + }); + + it('rejects managed catalog placeholders before Python executes', () => { + const validate = (IcebergDatalakeService as any) + .validateCatalogWarehousePair as (config: unknown) => void; + + ( + [ + 'glue', + 'biglake', + 'onelake', + 'unity', + 'snowflake', + 'cloudflare', + ] as const + ).forEach((catalogType) => { + expect(() => + validate({ catalogType, storageType: 'server-managed' }), + ).toThrow(`ICEBERG_CATALOG_NOT_ENABLED: ${catalogType}`); + }); + }); + it('validates Hive Thrift URIs and optional UGI identity', () => { const validate = (IcebergDatalakeService as any) .validateCatalogWarehousePair as (config: unknown) => void; From 4cb3fbfa562ff360299fb3067b9e92afe00af5e7 Mon Sep 17 00:00:00 2001 From: Nuri Lacka Date: Tue, 11 Aug 2026 13:40:52 +0200 Subject: [PATCH 08/12] feat(iceberg): implement tables and namespaces CRUD and view controls Implements Phase 4 of the Iceberg Data Lake integration (Plan 57d). Adds full self-service management surface for Iceberg tables and namespaces, including: - `import_table`, `drop_table`, `rename_table`, `create_namespace`, and `drop_namespace` operations in the Python bridge via PyArrow and PyIceberg. - Input validation in `IcebergDatalakeService` before spawning Python processes. - IPC channels and React Query controller hooks for operations and cache invalidation. - Data Lake UI enhancements: Import Data wizard, inline rename/delete actions, New Namespace dialog, namespace chips strip for filtering, and a flat/grouped view toggle. - Comprehensive Python bridge tests and Jest unit tests for service validation. --- resources/python/iceberg_bridge.py | 149 +++- .../icebergDatalake.ipcHandlers.ts | 31 +- src/main/services/icebergDatalake.service.ts | 231 +++++ .../dataLake/iceberg/IcebergDetail.tsx | 791 +++++++++++++++++- .../iceberg/IcebergOperationBackdrop.tsx | 44 + .../iceberg/IcebergTableImportWizard.tsx | 457 ++++++++++ .../controllers/icebergDatalake.controller.ts | 109 +++ src/renderer/screens/projectDetails/index.tsx | 23 + src/renderer/services/iceberg.service.ts | 53 ++ src/types/iceberg.ts | 36 + src/types/ipc.ts | 6 + tests/python/test_iceberg_bridge.py | 437 ++++++++++ .../services/icebergDatalake.service.test.ts | 157 ++++ 13 files changed, 2476 insertions(+), 48 deletions(-) create mode 100644 src/renderer/components/dataLake/iceberg/IcebergOperationBackdrop.tsx create mode 100644 src/renderer/components/dataLake/iceberg/IcebergTableImportWizard.tsx diff --git a/resources/python/iceberg_bridge.py b/resources/python/iceberg_bridge.py index 9ef94260..0965c79e 100644 --- a/resources/python/iceberg_bridge.py +++ b/resources/python/iceberg_bridge.py @@ -6,7 +6,9 @@ Supported commands: install_check, test_connection, list_namespaces, list_tables, - get_schema, get_snapshots, preview_table, create_metadata_file + get_schema, get_snapshots, preview_table, import_table, + drop_table, rename_table, create_namespace, drop_namespace, + create_metadata_file """ import json @@ -193,6 +195,146 @@ def handle_preview_table(cmd: dict) -> dict: return {"ok": False, "error": str(exc)} +def handle_import_table(cmd: dict) -> dict: + """ + Import a local CSV/Parquet/JSON file into a new Iceberg table. + + Reads the file with PyArrow, infers its schema, creates the table in the + requested namespace (creating the namespace when absent), and appends the + data as the initial snapshot. + """ + try: + import pyarrow as pa # noqa: PLC0415 + import pyarrow.csv as pa_csv # noqa: PLC0415 + import pyarrow.json as pa_json # noqa: PLC0415 + import pyarrow.parquet as pa_parquet # noqa: PLC0415 + from pyiceberg.catalog import load_catalog # noqa: PLC0415 + from pyiceberg.exceptions import TableAlreadyExistsError # noqa: PLC0415 + + props = resolve_env_vars(cmd.get("catalog_properties", {})) + catalog = load_catalog(cmd["catalog_name"], **props) + namespace = tuple(cmd["namespace"]) + table_name = cmd["table"] + identifier = (*namespace, table_name) + file_path = cmd["file_path"] + file_format = cmd.get("file_format", "").lower() + + if not os.path.isfile(file_path): + return {"ok": False, "error": f"File not found: {file_path}"} + + if file_format == "parquet": + arrow_table = pa_parquet.read_table(file_path) + elif file_format == "csv": + arrow_table = pa_csv.read_csv(file_path) + elif file_format == "json": + try: + arrow_table = pa_json.read_json(file_path) + except Exception as json_error: # noqa: BLE001 + # pyarrow.json.read_json only accepts newline-delimited records. + # Fall back to parsing a JSON array of objects when present. + with open(file_path, encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, list): + raise ValueError( + f"JSON source must be an array of objects or newline-delimited records: {json_error}" + ) from json_error + arrow_table = pa.Table.from_pylist(payload) + else: + return {"ok": False, "error": f"Unsupported file format: {file_format}"} + + if arrow_table.num_columns == 0: + return {"ok": False, "error": "The source file contains no columns."} + + try: + catalog.create_namespace_if_not_exists(namespace) + table = catalog.create_table(identifier, schema=arrow_table.schema) + except TableAlreadyExistsError: + return { + "ok": False, + "error": f"Table already exists: {'.'.join(namespace)}.{table_name}", + } + + try: + table.append(arrow_table) + except Exception: # noqa: BLE001 + # Best-effort cleanup of the empty table on write failure so a + # failed import does not leave a phantom catalog entry. + try: + catalog.drop_table(identifier) + except Exception: # noqa: BLE001 + pass + raise + + return { + "ok": True, + "namespace": list(namespace), + "table": table_name, + "row_count": arrow_table.num_rows, + "columns": arrow_table.schema.names, + } + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + +def handle_drop_table(cmd: dict) -> dict: + """Drop a table from the catalog.""" + try: + from pyiceberg.catalog import load_catalog # noqa: PLC0415 + props = resolve_env_vars(cmd.get("catalog_properties", {})) + catalog = load_catalog(cmd["catalog_name"], **props) + namespace = tuple(cmd["namespace"]) + catalog.drop_table((*namespace, cmd["table"])) + return {"ok": True, "namespace": list(namespace), "table": cmd["table"]} + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + +def handle_rename_table(cmd: dict) -> dict: + """Rename a table within the same namespace.""" + try: + from pyiceberg.catalog import load_catalog # noqa: PLC0415 + props = resolve_env_vars(cmd.get("catalog_properties", {})) + catalog = load_catalog(cmd["catalog_name"], **props) + namespace = tuple(cmd["namespace"]) + catalog.rename_table( + (*namespace, cmd["table"]), + (*namespace, cmd["new_table"]), + ) + return { + "ok": True, + "namespace": list(namespace), + "table": cmd["new_table"], + } + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + +def handle_create_namespace(cmd: dict) -> dict: + """Create a (possibly nested) namespace in the catalog.""" + try: + from pyiceberg.catalog import load_catalog # noqa: PLC0415 + props = resolve_env_vars(cmd.get("catalog_properties", {})) + catalog = load_catalog(cmd["catalog_name"], **props) + namespace = tuple(cmd["namespace"]) + catalog.create_namespace(namespace) + return {"ok": True, "namespace": list(namespace)} + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + +def handle_drop_namespace(cmd: dict) -> dict: + """Drop a namespace. The namespace must be empty (no tables).""" + try: + from pyiceberg.catalog import load_catalog # noqa: PLC0415 + props = resolve_env_vars(cmd.get("catalog_properties", {})) + catalog = load_catalog(cmd["catalog_name"], **props) + namespace = tuple(cmd["namespace"]) + catalog.drop_namespace(namespace) + return {"ok": True, "namespace": list(namespace)} + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + def handle_create_metadata_file(cmd: dict) -> dict: """ Initialize a development-only SQL catalog backed by SQLite. @@ -244,6 +386,11 @@ def handle_create_metadata_file(cmd: dict) -> dict: "get_schema": handle_get_schema, "get_snapshots": handle_get_snapshots, "preview_table": handle_preview_table, + "import_table": handle_import_table, + "drop_table": handle_drop_table, + "rename_table": handle_rename_table, + "create_namespace": handle_create_namespace, + "drop_namespace": handle_drop_namespace, "create_metadata_file": handle_create_metadata_file, } diff --git a/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts b/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts index fed4a0ae..4ede448a 100644 --- a/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts +++ b/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts @@ -53,13 +53,42 @@ export const registerIcebergDatalakeHandlers = () => { ipcMain.handle('iceberg:getSnapshots', (_e, id: string, namespace, table) => IcebergDatalakeService.getTableSnapshots(id, namespace, table), ); - ipcMain.handle( 'iceberg:previewTable', (_e, id: string, namespace, table, limit, filter?) => IcebergDatalakeService.previewTable(id, namespace, table, limit, filter), ); + ipcMain.handle( + 'iceberg:importTable', + (_e, id: string, namespace, table, filePath, fileFormat) => + IcebergDatalakeService.importTable( + id, + namespace, + table, + filePath, + fileFormat, + ), + ); + + ipcMain.handle('iceberg:dropTable', (_e, id: string, namespace, table) => + IcebergDatalakeService.dropTable(id, namespace, table), + ); + + ipcMain.handle( + 'iceberg:renameTable', + (_e, id: string, namespace, table, newTable) => + IcebergDatalakeService.renameTable(id, namespace, table, newTable), + ); + + ipcMain.handle('iceberg:createNamespace', (_e, id: string, namespace) => + IcebergDatalakeService.createNamespace(id, namespace), + ); + + ipcMain.handle('iceberg:dropNamespace', (_e, id: string, namespace) => + IcebergDatalakeService.dropNamespace(id, namespace), + ); + ipcMain.handle('iceberg:createMetadataFile', (_e, warehousePath: string) => IcebergDatalakeService.createMetadataFile(warehousePath), ); diff --git a/src/main/services/icebergDatalake.service.ts b/src/main/services/icebergDatalake.service.ts index a32a3174..28234a0a 100644 --- a/src/main/services/icebergDatalake.service.ts +++ b/src/main/services/icebergDatalake.service.ts @@ -7,6 +7,7 @@ * Follows BE-03 (one cohesive service), BE-04 (no await inside Promise constructor). */ +import * as fs from 'fs'; import { v4 as uuidv4 } from 'uuid'; import * as path from 'path'; import { pathToFileURL } from 'url'; @@ -31,6 +32,10 @@ import type { IcebergLocalCatalogResult, IcebergCapabilities, IcebergCatalogCapability, + IcebergImportTableResult, + IcebergImportFileFormat, + IcebergTableOperationResult, + IcebergNamespaceOperationResult, } from '../../types/iceberg'; import type { CloudConnection } from '../../types/frontend'; import type { PostgresConnection } from '../../types/backend'; @@ -1351,6 +1356,232 @@ export class IcebergDatalakeService { // Public: file helpers // ───────────────────────────────────────────── + // ───────────────────────────────────────────── + // Public: local file import + // ───────────────────────────────────────────── + + static async importTable( + id: string, + namespace: string[], + table: string, + filePath: string, + fileFormat: string, + ): Promise { + try { + const safeFormat = fileFormat?.toLowerCase() as + | IcebergImportFileFormat + | undefined; + if (!safeFormat || !['csv', 'parquet', 'json'].includes(safeFormat)) { + throw new Error('ICEBERG_IMPORT_FORMAT_UNSUPPORTED'); + } + const safeTable = table?.trim(); + if (!safeTable || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(safeTable)) { + throw new Error('ICEBERG_IMPORT_TABLE_NAME_INVALID'); + } + const safeNamespace = IcebergDatalakeService.validateNamespace(namespace); + if (!filePath?.trim()) { + throw new Error('ICEBERG_IMPORT_FILE_REQUIRED'); + } + if ( + !fs.existsSync(filePath.trim()) || + !fs.statSync(filePath.trim()).isFile() + ) { + throw new Error('ICEBERG_IMPORT_FILE_NOT_FOUND'); + } + + const instance = await IcebergDatalakeService.getInstance(id); + const { props, env } = + await IcebergDatalakeService.buildCatalogProperties(instance); + const result = (await IcebergDatalakeService.runBridge( + { + command: 'import_table', + catalog_name: + instance.catalogType === 'sqlite' + ? 'local' + : (instance.catalogName ?? id), + catalog_properties: props, + namespace: safeNamespace, + table: safeTable, + file_path: filePath.trim(), + file_format: safeFormat, + }, + env, + )) as Record; + return { + namespace: (result.namespace as string[]) ?? safeNamespace, + table: (result.table as string) ?? safeTable, + rowCount: Number(result.row_count ?? 0), + columns: (result.columns as string[]) ?? [], + }; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] importTable error:', error); + throw error; + } + } + + // ───────────────────────────────────────────── + // Public: table maintenance + // ───────────────────────────────────────────── + + static async dropTable( + id: string, + namespace: string[], + table: string, + ): Promise { + try { + const safeTable = table?.trim(); + if (!safeTable || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(safeTable)) { + throw new Error('ICEBERG_TABLE_NAME_INVALID'); + } + const safeNamespace = IcebergDatalakeService.validateNamespace(namespace); + const instance = await IcebergDatalakeService.getInstance(id); + const { props, env } = + await IcebergDatalakeService.buildCatalogProperties(instance); + const result = (await IcebergDatalakeService.runBridge( + { + command: 'drop_table', + catalog_name: + instance.catalogType === 'sqlite' + ? 'local' + : (instance.catalogName ?? id), + catalog_properties: props, + namespace: safeNamespace, + table: safeTable, + }, + env, + )) as Record; + return { + namespace: (result.namespace as string[]) ?? safeNamespace, + table: (result.table as string) ?? safeTable, + }; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] dropTable error:', error); + throw error; + } + } + + static async renameTable( + id: string, + namespace: string[], + table: string, + newTable: string, + ): Promise { + try { + const safeTable = table?.trim(); + if (!safeTable || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(safeTable)) { + throw new Error('ICEBERG_TABLE_NAME_INVALID'); + } + const safeNewTable = newTable?.trim(); + if (!safeNewTable || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(safeNewTable)) { + throw new Error('ICEBERG_TABLE_NAME_INVALID'); + } + if (safeNewTable === safeTable) { + throw new Error('ICEBERG_RENAME_SAME_NAME'); + } + const safeNamespace = IcebergDatalakeService.validateNamespace(namespace); + const instance = await IcebergDatalakeService.getInstance(id); + const { props, env } = + await IcebergDatalakeService.buildCatalogProperties(instance); + const result = (await IcebergDatalakeService.runBridge( + { + command: 'rename_table', + catalog_name: + instance.catalogType === 'sqlite' + ? 'local' + : (instance.catalogName ?? id), + catalog_properties: props, + namespace: safeNamespace, + table: safeTable, + new_table: safeNewTable, + }, + env, + )) as Record; + return { + namespace: (result.namespace as string[]) ?? safeNamespace, + table: (result.table as string) ?? safeNewTable, + }; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] renameTable error:', error); + throw error; + } + } + + static async createNamespace( + id: string, + namespace: string[], + ): Promise { + try { + const safeNamespace = IcebergDatalakeService.validateNamespace(namespace); + const instance = await IcebergDatalakeService.getInstance(id); + const { props, env } = + await IcebergDatalakeService.buildCatalogProperties(instance); + const result = (await IcebergDatalakeService.runBridge( + { + command: 'create_namespace', + catalog_name: + instance.catalogType === 'sqlite' + ? 'local' + : (instance.catalogName ?? id), + catalog_properties: props, + namespace: safeNamespace, + }, + env, + )) as Record; + return { + namespace: (result.namespace as string[]) ?? safeNamespace, + }; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] createNamespace error:', error); + throw error; + } + } + + static async dropNamespace( + id: string, + namespace: string[], + ): Promise { + try { + const safeNamespace = IcebergDatalakeService.validateNamespace(namespace); + const instance = await IcebergDatalakeService.getInstance(id); + const { props, env } = + await IcebergDatalakeService.buildCatalogProperties(instance); + const result = (await IcebergDatalakeService.runBridge( + { + command: 'drop_namespace', + catalog_name: + instance.catalogType === 'sqlite' + ? 'local' + : (instance.catalogName ?? id), + catalog_properties: props, + namespace: safeNamespace, + }, + env, + )) as Record; + return { + namespace: (result.namespace as string[]) ?? safeNamespace, + }; + } catch (error) { + // eslint-disable-next-line no-console + console.error('[IcebergDatalakeService] dropNamespace error:', error); + throw error; + } + } + + private static validateNamespace(namespace: string[]): string[] { + const safeNamespace = (namespace ?? []).map((part) => part.trim()); + if ( + safeNamespace.length === 0 || + safeNamespace.some((part) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(part)) + ) { + throw new Error('ICEBERG_NAMESPACE_INVALID'); + } + return safeNamespace; + } + static async createMetadataFile( warehousePath: string, ): Promise { diff --git a/src/renderer/components/dataLake/iceberg/IcebergDetail.tsx b/src/renderer/components/dataLake/iceberg/IcebergDetail.tsx index ba1b8543..a45d2462 100644 --- a/src/renderer/components/dataLake/iceberg/IcebergDetail.tsx +++ b/src/renderer/components/dataLake/iceberg/IcebergDetail.tsx @@ -5,6 +5,10 @@ import { Button, Chip, CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, FormControl, Grid, IconButton, @@ -27,17 +31,23 @@ import { TableRow, Tabs, TextField, + ToggleButton, + ToggleButtonGroup, Tooltip, Typography, } from '@mui/material'; import { AccountTree, + Add, ArrowBack, Badge, ChevronRight, CheckCircle, + Close, CloudQueue, + CreateNewFolder, Delete, + DriveFileRenameOutline, Edit, ErrorOutline, Folder, @@ -57,17 +67,27 @@ import { cloudStorageImages, databaseIcons, } from '../../../../../assets/connectionIcons'; -import type { IcebergInstanceConfig } from '../../../../types/iceberg'; +import type { + IcebergImportFileFormat, + IcebergInstanceConfig, +} from '../../../../types/iceberg'; import { + useCreateIcebergNamespace, + useDropIcebergNamespace, + useDropIcebergTable, useGetIcebergInstance, useGetIcebergSchema, useGetIcebergSnapshots, useIcebergTablePreview, + useImportIcebergTable, useListIcebergNamespaces, useListIcebergTables, + useRenameIcebergTable, useTestIcebergInstance, } from '../../../controllers/icebergDatalake.controller'; import { IcebergIcon } from './IcebergIcon'; +import { IcebergOperationBackdrop } from './IcebergOperationBackdrop'; +import { IcebergTableImportWizard } from './IcebergTableImportWizard'; interface IcebergDetailProps { instance: IcebergInstanceConfig; @@ -84,7 +104,10 @@ interface NamespaceTableRowsProps { instanceId: string; namespace: string[]; filter: string; + showEmptyRow?: boolean; onSelect: (selection: SelectedTable) => void; + onDelete: (selection: SelectedTable) => void; + onRename: (selection: SelectedTable) => void; } const errorMessage = (error: unknown): string => @@ -122,7 +145,9 @@ const IcebergTableRow: React.FC<{ namespace: string[]; table: string; onSelect: (selection: SelectedTable) => void; -}> = ({ instanceId, namespace, table, onSelect }) => { + onDelete: (selection: SelectedTable) => void; + onRename: (selection: SelectedTable) => void; +}> = ({ instanceId, namespace, table, onSelect, onDelete, onRename }) => { const snapshotsQuery = useGetIcebergSnapshots(instanceId, namespace, table); const snapshots = snapshotsQuery.data ?? []; const firstSnapshot = snapshots[0]; @@ -164,17 +189,44 @@ const IcebergTableRow: React.FC<{ : '—'} - - { - event.stopPropagation(); - onSelect({ namespace, table }); - }} - > - - - + + + { + event.stopPropagation(); + onRename({ namespace, table }); + }} + > + + + + + { + event.stopPropagation(); + onDelete({ namespace, table }); + }} + > + + + + + { + event.stopPropagation(); + onSelect({ namespace, table }); + }} + > + + + + ); @@ -184,7 +236,10 @@ const NamespaceTableRows: React.FC = ({ instanceId, namespace, filter, + showEmptyRow = false, onSelect, + onDelete, + onRename, }) => { const tablesQuery = useListIcebergTables(instanceId, namespace); const normalizedFilter = filter.trim().toLowerCase(); @@ -215,12 +270,64 @@ const NamespaceTableRows: React.FC = ({ namespace={namespace} table={table} onSelect={onSelect} + onDelete={onDelete} + onRename={onRename} /> ))} + {showEmptyRow && tables.length === 0 && ( + + + No tables found in {namespace.join('.')}. + + + )} ); }; +const NamespaceGroupHeader: React.FC<{ + instanceId: string; + namespace: string[]; + onDrop: (namespace: string[]) => void; +}> = ({ instanceId, namespace, onDrop }) => { + const tablesQuery = useListIcebergTables(instanceId, namespace); + const count = tablesQuery.data?.length ?? 0; + return ( + + + + + + {namespace.join('.')} + + + + + onDrop(namespace)} + > + + + + + + + ); +}; + function TableHistoryRows({ instanceId, namespace, @@ -737,8 +844,182 @@ export const IcebergDetail: React.FC = ({ const queryClient = useQueryClient(); const namespacesQuery = useListIcebergNamespaces(instance.id); const testInstanceMutation = useTestIcebergInstance(); + const importTableMutation = useImportIcebergTable(); + const dropTableMutation = useDropIcebergTable(); + const renameTableMutation = useRenameIcebergTable(); + const createNamespaceMutation = useCreateIcebergNamespace(); + const dropNamespaceMutation = useDropIcebergNamespace(); const [currentTab, setCurrentTab] = React.useState(0); const [tableFilter, setTableFilter] = React.useState(''); + const [importWizardOpen, setImportWizardOpen] = React.useState(false); + const [tableToDelete, setTableToDelete] = + React.useState(null); + const [tableToRename, setTableToRename] = + React.useState(null); + const [newTableName, setNewTableName] = React.useState(''); + const [namespaceCreateOpen, setNamespaceCreateOpen] = React.useState(false); + const [newNamespaceName, setNewNamespaceName] = React.useState(''); + const [namespaceToDelete, setNamespaceToDelete] = React.useState< + string[] | null + >(null); + const [namespaceFilter, setNamespaceFilter] = React.useState( + null, + ); + const [groupByNamespace, setGroupByNamespace] = React.useState(false); + const namespaceNameValid = (() => { + const parts = newNamespaceName + .trim() + .split('.') + .map((part) => part.trim()) + .filter(Boolean); + return ( + parts.length > 0 && + parts.every((part) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(part)) + ); + })(); + + const handleDeleteTable = () => { + if (!tableToDelete) return; + dropTableMutation.mutate( + { + id: instance.id, + namespace: tableToDelete.namespace, + table: tableToDelete.table, + }, + { + onSuccess: (result) => { + setTableToDelete(null); + toast.success( + `Deleted ${result.namespace.join('.')}.${result.table}`, + ); + }, + onError: (error) => { + toast.error(errorMessage(error)); + }, + }, + ); + }; + + const handleRenameTable = () => { + if (!tableToRename) return; + const trimmed = newTableName.trim(); + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) { + toast.error( + 'Table name must start with a letter or underscore and contain only letters, numbers, and underscores', + ); + return; + } + renameTableMutation.mutate( + { + id: instance.id, + namespace: tableToRename.namespace, + table: tableToRename.table, + newTable: trimmed, + }, + { + onSuccess: (result) => { + setTableToRename(null); + setNewTableName(''); + toast.success( + `Renamed to ${result.namespace.join('.')}.${result.table}`, + ); + }, + onError: (error) => { + toast.error(errorMessage(error)); + }, + }, + ); + }; + + const handleCreateNamespace = () => { + const parts = newNamespaceName + .trim() + .split('.') + .map((part) => part.trim()) + .filter(Boolean); + if ( + parts.length === 0 || + parts.some((part) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(part)) + ) { + toast.error( + 'Namespace parts must start with a letter or underscore and contain only letters, numbers, and underscores', + ); + return; + } + const fullName = parts.join('.'); + if (namespacesQuery.data?.some((ns) => ns.join('.') === fullName)) { + toast.error(`Namespace ${fullName} already exists`); + return; + } + createNamespaceMutation.mutate( + { + id: instance.id, + namespace: parts, + }, + { + onSuccess: (result) => { + setNamespaceCreateOpen(false); + setNewNamespaceName(''); + toast.success(`Created namespace ${result.namespace.join('.')}`); + }, + onError: (error) => { + toast.error(errorMessage(error)); + }, + }, + ); + }; + + const handleDropNamespace = () => { + if (!namespaceToDelete) return; + dropNamespaceMutation.mutate( + { + id: instance.id, + namespace: namespaceToDelete, + }, + { + onSuccess: (result) => { + setNamespaceToDelete(null); + // If the dropped namespace was the active filter, clear it so the + // table body does not render empty with no explanation. + setNamespaceFilter((current) => + current === result.namespace.join('.') ? null : current, + ); + toast.success(`Dropped namespace ${result.namespace.join('.')}`); + }, + onError: (error) => { + toast.error(errorMessage(error)); + }, + }, + ); + }; + + const handleImportTable = ( + namespace: string[], + table: string, + filePath: string, + fileFormat: IcebergImportFileFormat, + ) => { + importTableMutation.mutate( + { + id: instance.id, + namespace, + table, + filePath, + fileFormat, + }, + { + onSuccess: (result) => { + setImportWizardOpen(false); + toast.success( + `Imported ${result.rowCount} rows into ${result.namespace.join('.')}.${result.table}`, + ); + }, + onError: (error) => { + toast.error(errorMessage(error)); + }, + }, + ); + }; const refresh = async () => { await queryClient.invalidateQueries(['iceberg', 'namespaces', instance.id]); @@ -840,37 +1121,167 @@ export const IcebergDetail: React.FC = ({ ); } if (namespacesQuery.data?.length === 0) { - return No namespaces or tables found.; + return ( + + + No namespaces or tables found. Create a namespace or import a local + file to create your first table. + + + + + + + ); } return ( <> - setTableFilter(event.target.value)} - placeholder="Search by name or schema…" - slotProps={{ - input: { - startAdornment: ( - - - - ), - sx: { fontSize: '0.8125rem', height: '32px' }, - }, + + setTableFilter(event.target.value)} + placeholder="Search by name or schema…" + slotProps={{ + input: { + startAdornment: ( + + + + ), + sx: { fontSize: '0.8125rem', height: '32px' }, + }, + }} + sx={{ + width: 280, + '& .MuiInputBase-input': { + paddingTop: '2px', + paddingBottom: '2px', + }, + '& .MuiOutlinedInput-root': { + minHeight: '32px', + }, + }} + /> + + + setGroupByNamespace(value === 'grouped') + } + sx={{ height: '32px' }} + > + + + + + + + + + + + + + + + + + > + + Namespaces: + + + } + label="All" + size="small" + color={!namespaceFilter ? 'primary' : 'default'} + variant={!namespaceFilter ? 'filled' : 'outlined'} + onClick={() => setNamespaceFilter(null)} + /> + + {namespacesQuery.data?.map((namespace) => { + const fullName = namespace.join('.'); + const active = namespaceFilter === fullName; + return ( + + } + label={fullName} + size="small" + color={active ? 'primary' : 'default'} + variant={active ? 'filled' : 'outlined'} + onClick={() => setNamespaceFilter(active ? null : fullName)} + onDelete={() => setNamespaceToDelete(namespace)} + deleteIcon={ + + } + /> + + ); + })} + @@ -886,15 +1297,48 @@ export const IcebergDetail: React.FC = ({ - {namespacesQuery.data?.map((namespace) => ( - - ))} + {namespacesQuery.data + ?.filter( + (namespace) => + !namespaceFilter || namespace.join('.') === namespaceFilter, + ) + .map((namespace) => + groupByNamespace ? ( + + setNamespaceToDelete(ns)} + /> + setTableToDelete(selection)} + onRename={(selection) => { + setTableToRename(selection); + setNewTableName(selection.table); + }} + /> + + ) : ( + setTableToDelete(selection)} + onRename={(selection) => { + setTableToRename(selection); + setNewTableName(selection.table); + }} + /> + ), + )}
@@ -902,6 +1346,46 @@ export const IcebergDetail: React.FC = ({ ); }; + const renderImportWizard = () => ( + setImportWizardOpen(false)} + instanceId={instance.id} + onImport={handleImportTable} + isLoading={importTableMutation.isLoading} + /> + ); + + // Operations that lock the UI behind a full-screen backdrop until they + // finish, so the user cannot double-submit, misclick, or navigate away + // mid-operation. The first in-flight operation's label is shown. + const blockingOperations = [ + { + isLoading: importTableMutation.isLoading, + label: 'Importing table…', + }, + { + isLoading: dropTableMutation.isLoading, + label: 'Deleting table…', + }, + { + isLoading: renameTableMutation.isLoading, + label: 'Renaming table…', + }, + { + isLoading: createNamespaceMutation.isLoading, + label: 'Creating namespace…', + }, + { + isLoading: dropNamespaceMutation.isLoading, + label: 'Dropping namespace…', + }, + { + isLoading: testInstanceMutation.isLoading, + label: 'Testing connection…', + }, + ]; + let catalogIdentityLabel = 'Catalog Name'; let catalogIdentityValue = instance.catalogName ?? 'Local catalog'; if (instance.catalogType === 'nessie') { @@ -1172,6 +1656,221 @@ export const IcebergDetail: React.FC = ({ )}
+ + {renderImportWizard()} + + {/* Delete table confirmation dialog */} + setTableToDelete(null)} + maxWidth="sm" + fullWidth + > + Delete table + + + Are you sure you want to delete{' '} + + {tableToDelete?.namespace.join('.')}.{tableToDelete?.table} + + ? This removes the table and its metadata from the catalog. + + + + + + + + + {/* Rename table dialog */} + { + setTableToRename(null); + setNewTableName(''); + }} + maxWidth="sm" + fullWidth + > + Rename table + + + Rename{' '} + + {tableToRename?.namespace.join('.')}.{tableToRename?.table} + + + setNewTableName(e.target.value)} + disabled={renameTableMutation.isLoading} + autoFocus + /> + + + + + + + + {/* Create namespace dialog */} + { + setNamespaceCreateOpen(false); + setNewNamespaceName(''); + }} + maxWidth="sm" + fullWidth + > + Create namespace + + + Namespaces organize tables. Use dot notation for nested namespaces + (for example analytics.daily). + + setNewNamespaceName(e.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter' && !createNamespaceMutation.isLoading) { + handleCreateNamespace(); + } + }} + disabled={createNamespaceMutation.isLoading} + helperText="Letters, numbers, underscores, and dots only" + /> + + + {' '} + + + + + {/* Drop namespace confirmation dialog */} + setNamespaceToDelete(null)} + maxWidth="sm" + fullWidth + > + Drop namespace + + + Are you sure you want to drop namespace{' '} + {namespaceToDelete?.join('.')}? + + + The namespace must be empty. Delete its tables first, otherwise the + catalog will reject the drop. Dropping a parent namespace also + removes its nested namespaces. + + + + + + + + + {/* Full-screen lock while any Iceberg operation is in flight */} +
); }; diff --git a/src/renderer/components/dataLake/iceberg/IcebergOperationBackdrop.tsx b/src/renderer/components/dataLake/iceberg/IcebergOperationBackdrop.tsx new file mode 100644 index 00000000..9b7dee0b --- /dev/null +++ b/src/renderer/components/dataLake/iceberg/IcebergOperationBackdrop.tsx @@ -0,0 +1,44 @@ +import { Backdrop, CircularProgress, Typography } from '@mui/material'; +import React from 'react'; + +export interface IcebergOperation { + /** Whether this operation is currently in flight. */ + isLoading: boolean; + /** Label shown on the backdrop while the operation runs. */ + label: string; +} + +interface IcebergOperationBackdropProps { + /** The operations that can block the UI; the first in-flight one wins. */ + operations: IcebergOperation[]; +} + +/** + * Full-screen loading backdrop that locks the UI while an Iceberg operation + * (import, rename, delete, namespace create/drop, connection test) is running. + * Rendered above everything (drawer + 999) so the user cannot interact with + * the app or navigate until the operation finishes and the UI is unlocked. + */ +export const IcebergOperationBackdrop: React.FC< + IcebergOperationBackdropProps +> = ({ operations }) => { + const active = operations.find((operation) => operation.isLoading); + return ( + theme.zIndex.drawer + 999, // above everything incl. sidebar + dialogs + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 2, + }} + > + + + {active?.label ?? 'Working…'} + + + ); +}; diff --git a/src/renderer/components/dataLake/iceberg/IcebergTableImportWizard.tsx b/src/renderer/components/dataLake/iceberg/IcebergTableImportWizard.tsx new file mode 100644 index 00000000..1214f5e5 --- /dev/null +++ b/src/renderer/components/dataLake/iceberg/IcebergTableImportWizard.tsx @@ -0,0 +1,457 @@ +import React, { useState } from 'react'; +import { + Box, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Stepper, + Step, + StepLabel, + TextField, + Typography, + Alert, + Card, + CardContent, + CardActionArea, + IconButton, + CircularProgress, + Autocomplete, + Chip, +} from '@mui/material'; +import { + ArrowBack, + ArrowForward, + Folder, + FolderOpen, + Close, + CloudDownload, + TableChart, +} from '@mui/icons-material'; +import { useFilePicker } from '../../../controllers/settings.controller'; +import { useListIcebergNamespaces } from '../../../controllers/icebergDatalake.controller'; +import type { IcebergImportFileFormat } from '../../../../types/iceberg'; + +interface IcebergTableImportWizardProps { + open: boolean; + onClose: () => void; + onImport: ( + namespace: string[], + tableName: string, + filePath: string, + fileFormat: IcebergImportFileFormat, + ) => void; + isLoading?: boolean; + instanceId: string; +} + +const steps = ['Select Source', 'Configure Import', 'Review']; + +const SUPPORTED_EXTENSIONS: { + ext: string; + format: IcebergImportFileFormat; +}[] = [ + { ext: 'csv', format: 'csv' }, + { ext: 'parquet', format: 'parquet' }, + { ext: 'pq', format: 'parquet' }, + { ext: 'json', format: 'json' }, +]; + +const detectFormat = ( + filePath: string, +): IcebergImportFileFormat | undefined => { + const ext = filePath.toLowerCase().split('.').pop() ?? ''; + return SUPPORTED_EXTENSIONS.find((item) => item.ext === ext)?.format; +}; + +export const IcebergTableImportWizard: React.FC< + IcebergTableImportWizardProps +> = ({ open, onClose, onImport, isLoading = false, instanceId }) => { + const [activeStep, setActiveStep] = useState(0); + const [tableName, setTableName] = useState(''); + const [filePath, setFilePath] = useState(''); + const [selectedNamespace, setSelectedNamespace] = useState( + '__new__', + ); + const [newNamespace, setNewNamespace] = useState('default'); + const [error, setError] = useState(''); + + const { mutate: getFiles } = useFilePicker(); + const namespacesQuery = useListIcebergNamespaces(instanceId); + const namespaces = (namespacesQuery.data ?? []).map((ns) => ns.join('.')); + + const isCreatingNamespace = + selectedNamespace === '__new__' || selectedNamespace === null; + + const handleFileSelect = () => { + getFiles( + { + properties: ['openFile'], + filters: [ + { name: 'Data Files', extensions: ['csv', 'parquet', 'pq', 'json'] }, + { name: 'All Files', extensions: ['*'] }, + ], + }, + { + onSuccess: (filePaths) => { + if (filePaths && filePaths.length > 0) { + setFilePath(filePaths[0]); + } + }, + }, + ); + }; + + const handleNext = () => { + if (activeStep === 1) { + if (!tableName.trim()) { + setError('Table name is required'); + return; + } + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName.trim())) { + setError( + 'Table name must start with a letter or underscore and contain only letters, numbers, and underscores', + ); + return; + } + if (!filePath.trim()) { + setError('File path is required'); + return; + } + if (!detectFormat(filePath)) { + setError('Unsupported file type. Use CSV, Parquet, or JSON.'); + return; + } + if (isCreatingNamespace) { + const namespaceParts = newNamespace + .split('.') + .map((part) => part.trim()) + .filter(Boolean); + if ( + namespaceParts.length === 0 || + namespaceParts.some((part) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(part)) + ) { + setError( + 'Namespace must be dot-separated identifiers (letters, numbers, underscores)', + ); + return; + } + } + } + setError(''); + setActiveStep((prev) => prev + 1); + }; + + const handleBack = () => { + setError(''); + setActiveStep((prev) => prev - 1); + }; + + const handleImport = () => { + const namespace: string[] = isCreatingNamespace + ? newNamespace + .split('.') + .map((part) => part.trim()) + .filter(Boolean) + : (selectedNamespace ?? 'default').split('.'); + const format = detectFormat(filePath); + if (!format) return; + onImport(namespace, tableName.trim(), filePath.trim(), format); + }; + + const resetForm = () => { + setActiveStep(0); + setTableName(''); + setFilePath(''); + setSelectedNamespace('__new__'); + setNewNamespace('default'); + setError(''); + }; + + const renderSourceSelection = () => ( + + + Choose where to import data from + + + + + + + Local File + + + Import from local file system + + + Supports: CSV, Parquet, JSON + + + + + + ); + + const renderConfigureImport = () => ( + + setTableName(e.target.value)} + placeholder="e.g., customers, orders, products" + helperText="Enter a valid Iceberg table name" + autoFocus + sx={{ mb: 3 }} + /> + + { + setSelectedNamespace(value ?? null); + if (value === '__new__' || value === null) { + setNewNamespace('default'); + } + }} + getOptionLabel={(option) => + option === '__new__' ? 'Create new namespace…' : option + } + isOptionEqualToValue={(option, value) => option === value} + renderOption={(props, option) => ( +
  • + {option === '__new__' ? 'Create new namespace…' : option} +
  • + )} + renderInput={(params) => ( + + )} + /> + + {isCreatingNamespace && ( + setNewNamespace(e.target.value)} + placeholder="e.g., default or analytics.raw" + helperText="Nested namespaces are separated by dots" + sx={{ mb: 3 }} + /> + )} + + setFilePath(e.target.value)} + placeholder="/path/to/file.csv" + helperText="Absolute path to CSV, Parquet, or JSON file" + slotProps={{ + input: { + endAdornment: ( + + + + ), + }, + }} + /> + {filePath && ( + + + + )} + + + + PyIceberg will automatically infer the file schema, create the table, + and append the data as the initial snapshot. + + +
    + ); + + const renderReview = () => { + const namespace = isCreatingNamespace + ? newNamespace + .split('.') + .map((part) => part.trim()) + .filter(Boolean) + : (selectedNamespace ?? 'default').split('.'); + return ( + + + Review Import Configuration + + + + + Table Identifier + + + {namespace.join('.')}.{tableName} + + + + Source File + + + {filePath || 'Unavailable'} + + + + Detected Format + + + {detectFormat(filePath)?.toUpperCase() ?? '—'} + + + + + + What will happen: + + + 1. PyIceberg reads the source file and infers the schema +
    + 2. The namespace is created if it does not exist +
    + 3. An Iceberg table is created with the inferred schema +
    + 4. Data is appended and an initial snapshot is committed +
    +
    + + + + This operation may take some time depending on the data size. + + +
    + ); + }; + + const renderStepContent = () => { + switch (activeStep) { + case 0: + return renderSourceSelection(); + case 1: + return renderConfigureImport(); + case 2: + return renderReview(); + default: + return null; + } + }; + + return ( + + + + Import Data to Iceberg + + + + {steps.map((label) => ( + + {label} + + ))} + + + {error && ( + setError('')}> + {error} + + )} + + {renderStepContent()} + + + + {activeStep > 0 && ( + + )} + {activeStep < steps.length - 1 ? ( + + ) : ( + + )} + + + ); +}; diff --git a/src/renderer/controllers/icebergDatalake.controller.ts b/src/renderer/controllers/icebergDatalake.controller.ts index 3dafa53a..9c97672b 100644 --- a/src/renderer/controllers/icebergDatalake.controller.ts +++ b/src/renderer/controllers/icebergDatalake.controller.ts @@ -8,6 +8,9 @@ import React from 'react'; import { useMutation, useQuery, useQueryClient } from 'react-query'; import type { CreateIcebergInstanceDTO, + IcebergCreateNamespaceParams, + IcebergDropNamespaceParams, + IcebergImportTableParams, IcebergTestCatalogParams, } from '../../types/iceberg'; import * as icebergService from '../services/iceberg.service'; @@ -137,6 +140,36 @@ export const useCreateIcebergMetadataFile = () => icebergService.createIcebergMetadataFile(warehousePath), ); +export const useImportIcebergTable = () => { + const qc = useQueryClient(); + return useMutation( + ({ + id, + namespace, + table, + filePath, + fileFormat, + }: IcebergImportTableParams) => + icebergService.importIcebergTable( + id, + namespace, + table, + filePath, + fileFormat, + ), + + { + onSuccess: (_result, { id }) => { + qc.invalidateQueries(['iceberg', 'tables', id]); + qc.invalidateQueries(['iceberg', 'schema', id]); + qc.invalidateQueries(['iceberg', 'snapshots', id]); + qc.invalidateQueries(['iceberg', 'preview', id]); + qc.invalidateQueries(['iceberg', 'namespaces', id]); + }, + }, + ); +}; + export const usePreviewIcebergTable = () => useMutation( ({ @@ -161,6 +194,82 @@ export const usePreviewIcebergTable = () => ), ); +export const useDropIcebergTable = () => { + const qc = useQueryClient(); + return useMutation( + ({ + id, + namespace, + table, + }: { + id: string; + namespace: string[]; + table: string; + }) => icebergService.dropIcebergTable(id, namespace, table), + { + onSuccess: (_result, { id }) => { + qc.invalidateQueries(['iceberg', 'tables', id]); + qc.invalidateQueries(['iceberg', 'schema', id]); + qc.invalidateQueries(['iceberg', 'snapshots', id]); + qc.invalidateQueries(['iceberg', 'preview', id]); + }, + }, + ); +}; + +export const useRenameIcebergTable = () => { + const qc = useQueryClient(); + return useMutation( + ({ + id, + namespace, + table, + newTable, + }: { + id: string; + namespace: string[]; + table: string; + newTable: string; + }) => icebergService.renameIcebergTable(id, namespace, table, newTable), + { + onSuccess: (_result, { id }) => { + qc.invalidateQueries(['iceberg', 'tables', id]); + qc.invalidateQueries(['iceberg', 'schema', id]); + qc.invalidateQueries(['iceberg', 'snapshots', id]); + qc.invalidateQueries(['iceberg', 'preview', id]); + }, + }, + ); +}; + +export const useCreateIcebergNamespace = () => { + const qc = useQueryClient(); + return useMutation( + ({ id, namespace }: IcebergCreateNamespaceParams) => + icebergService.createIcebergNamespace(id, namespace), + { + onSuccess: (_result, { id }) => { + qc.invalidateQueries(['iceberg', 'namespaces', id]); + qc.invalidateQueries(['iceberg', 'tables', id]); + }, + }, + ); +}; + +export const useDropIcebergNamespace = () => { + const qc = useQueryClient(); + return useMutation( + ({ id, namespace }: IcebergDropNamespaceParams) => + icebergService.dropIcebergNamespace(id, namespace), + { + onSuccess: (_result, { id }) => { + qc.invalidateQueries(['iceberg', 'namespaces', id]); + qc.invalidateQueries(['iceberg', 'tables', id]); + }, + }, + ); +}; + export const useEnsureIcebergInstalled = () => useMutation(() => icebergService.ensureIcebergInstalled()); diff --git a/src/renderer/screens/projectDetails/index.tsx b/src/renderer/screens/projectDetails/index.tsx index 32502b71..457b9a81 100644 --- a/src/renderer/screens/projectDetails/index.tsx +++ b/src/renderer/screens/projectDetails/index.tsx @@ -266,6 +266,29 @@ const ProjectDetails: React.FC = () => { setPipelineDraftTab(null); }, [activePipelineFilePath]); + // Keep the code-mode draft in sync when the pipeline file is refreshed + // externally (e.g. after an agent update or a pipeline override). Only a + // CLEAN draft is overwritten — unsaved edits are preserved so a later user + // save cannot clobber the agent's change with stale YAML. + React.useEffect(() => { + if ( + !pipelineCodeMode || + !pipelineDraftTab || + activePipelineContent === undefined + ) { + return; + } + setPipelineDraftTab((prev) => { + if (!prev || prev.isModified) return prev; + if (activePipelineContent === prev.content) return prev; + return { + ...prev, + content: activePipelineContent, + savedContent: activePipelineContent, + }; + }); + }, [activePipelineContent, pipelineCodeMode, pipelineDraftTab]); + // Cloud logs auto-minimize once when a pipeline is first opened (mirrors // the terminal's own minimize/restore) and can be freely restored // afterward — restoring doesn't get overridden by mode changes. diff --git a/src/renderer/services/iceberg.service.ts b/src/renderer/services/iceberg.service.ts index 13db68a4..338284be 100644 --- a/src/renderer/services/iceberg.service.ts +++ b/src/renderer/services/iceberg.service.ts @@ -15,6 +15,10 @@ import type { IcebergPreviewResult, IcebergLocalCatalogResult, IcebergCapabilities, + IcebergImportTableResult, + IcebergImportFileFormat, + IcebergTableOperationResult, + IcebergNamespaceOperationResult, } from '../../types/iceberg'; export const getIcebergCapabilities = (): Promise => @@ -97,6 +101,55 @@ export const previewIcebergTable = ( rowFilter, ); +export const importIcebergTable = ( + id: string, + namespace: string[], + table: string, + filePath: string, + fileFormat: IcebergImportFileFormat, +): Promise => + window.electron.ipcRenderer.invoke( + 'iceberg:importTable', + id, + namespace, + table, + filePath, + fileFormat, + ); + +export const dropIcebergTable = ( + id: string, + namespace: string[], + table: string, +): Promise => + window.electron.ipcRenderer.invoke('iceberg:dropTable', id, namespace, table); + +export const renameIcebergTable = ( + id: string, + namespace: string[], + table: string, + newTable: string, +): Promise => + window.electron.ipcRenderer.invoke( + 'iceberg:renameTable', + id, + namespace, + table, + newTable, + ); + +export const createIcebergNamespace = ( + id: string, + namespace: string[], +): Promise => + window.electron.ipcRenderer.invoke('iceberg:createNamespace', id, namespace); + +export const dropIcebergNamespace = ( + id: string, + namespace: string[], +): Promise => + window.electron.ipcRenderer.invoke('iceberg:dropNamespace', id, namespace); + export const createIcebergMetadataFile = ( warehousePath: string, ): Promise => diff --git a/src/types/iceberg.ts b/src/types/iceberg.ts index 85bc1e1e..d2ea8dff 100644 --- a/src/types/iceberg.ts +++ b/src/types/iceberg.ts @@ -144,6 +144,20 @@ export interface IcebergPreviewResult { total?: number; } +export type IcebergImportFileFormat = 'csv' | 'parquet' | 'json'; + +export interface IcebergImportTableResult { + namespace: string[]; + table: string; + rowCount: number; + columns: string[]; +} + +export interface IcebergTableOperationResult { + namespace: string[]; + table: string; +} + export interface IcebergTestResult { success: boolean; error?: string; @@ -161,6 +175,28 @@ export interface IcebergLocalCatalogResult { tables: string[][]; } +export interface IcebergImportTableParams { + id: string; + namespace: string[]; + table: string; + filePath: string; + fileFormat: IcebergImportFileFormat; +} + +export interface IcebergNamespaceOperationResult { + namespace: string[]; +} + +export interface IcebergCreateNamespaceParams { + id: string; + namespace: string[]; +} + +export interface IcebergDropNamespaceParams { + id: string; + namespace: string[]; +} + export interface IcebergTestCatalogParams { catalogType: IcebergCatalogType; catalogPath?: string; diff --git a/src/types/ipc.ts b/src/types/ipc.ts index 767240de..a7f67f80 100644 --- a/src/types/ipc.ts +++ b/src/types/ipc.ts @@ -355,11 +355,17 @@ export type IcebergChannels = | 'iceberg:update' | 'iceberg:delete' | 'iceberg:testCatalog' + | 'iceberg:testInstance' | 'iceberg:listNamespaces' | 'iceberg:listTables' | 'iceberg:getSchema' | 'iceberg:getSnapshots' | 'iceberg:previewTable' + | 'iceberg:importTable' + | 'iceberg:dropTable' + | 'iceberg:renameTable' + | 'iceberg:createNamespace' + | 'iceberg:dropNamespace' | 'iceberg:createMetadataFile' | 'iceberg:ensureInstalled'; diff --git a/tests/python/test_iceberg_bridge.py b/tests/python/test_iceberg_bridge.py index 26b47096..636f7691 100644 --- a/tests/python/test_iceberg_bridge.py +++ b/tests/python/test_iceberg_bridge.py @@ -1,6 +1,8 @@ """End-to-end verification for the local PyIceberg SQLite catalog.""" +import csv as csv_module import importlib.util +import json import tempfile import unittest from pathlib import Path @@ -60,6 +62,441 @@ def test_create_reload_and_list_namespace_and_table(self) -> None: self.assertEqual(namespaces, {"ok": True, "namespaces": [["default"]]}) self.assertEqual(tables, {"ok": True, "tables": ["generated_data"]}) + def test_import_table_from_csv_creates_and_appends(self) -> None: + """Importing a local CSV creates a table and appends its rows.""" + with tempfile.TemporaryDirectory() as catalog_directory: + created = ICEBERG_BRIDGE.handle_create_metadata_file( + {"warehouse_path": catalog_directory} + ) + self.assertTrue(created["ok"]) + + source = Path(catalog_directory) / "source.csv" + with source.open("w", newline="", encoding="utf-8") as handle: + writer = csv_module.writer(handle) + writer.writerow(["id", "name"]) + writer.writerow(["1", "alpha"]) + writer.writerow(["2", "beta"]) + writer.writerow(["3", "gamma"]) + + properties = { + "type": "sql", + "uri": f"sqlite:///{created['metadata_path']}", + "warehouse": Path(created["warehouse_path"]).as_uri(), + } + imported = ICEBERG_BRIDGE.handle_import_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "imported_csv", + "file_path": str(source), + "file_format": "csv", + } + ) + self.assertTrue(imported["ok"], imported.get("error")) + self.assertEqual(imported["row_count"], 3) + self.assertEqual(imported["table"], "imported_csv") + self.assertEqual(imported["namespace"], ["default"]) + + tables = ICEBERG_BRIDGE.handle_list_tables( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + } + ) + self.assertIn("imported_csv", tables["tables"]) + + snapshots = ICEBERG_BRIDGE.handle_get_snapshots( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "imported_csv", + } + ) + self.assertTrue(snapshots["ok"]) + self.assertEqual(len(snapshots["snapshots"]), 1) + + def test_import_table_from_json_and_parquet(self) -> None: + """JSON and Parquet sources import with the same persisted contract.""" + with tempfile.TemporaryDirectory() as catalog_directory: + created = ICEBERG_BRIDGE.handle_create_metadata_file( + {"warehouse_path": catalog_directory} + ) + self.assertTrue(created["ok"]) + + properties = { + "type": "sql", + "uri": f"sqlite:///{created['metadata_path']}", + "warehouse": Path(created["warehouse_path"]).as_uri(), + } + + json_source = Path(catalog_directory) / "source.json" + # PyArrow's JSON reader expects newline-delimited JSON records. + json_source.write_text( + "\n".join( + json.dumps(record) + for record in [ + {"id": 1, "name": "alpha"}, + {"id": 2, "name": "beta"}, + ] + ), + encoding="utf-8", + ) + imported_json = ICEBERG_BRIDGE.handle_import_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "imported_json", + "file_path": str(json_source), + "file_format": "json", + } + ) + self.assertTrue(imported_json["ok"], imported_json.get("error")) + self.assertEqual(imported_json["row_count"], 2) + + json_array_source = Path(catalog_directory) / "array.json" + json_array_source.write_text( + json.dumps( + [ + {"id": 3, "name": "delta"}, + {"id": 4, "name": "epsilon"}, + ] + ), + encoding="utf-8", + ) + imported_json_array = ICEBERG_BRIDGE.handle_import_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "imported_json_array", + "file_path": str(json_array_source), + "file_format": "json", + } + ) + self.assertTrue( + imported_json_array["ok"], imported_json_array.get("error") + ) + self.assertEqual(imported_json_array["row_count"], 2) + + parquet_source = Path(catalog_directory) / "source.parquet" + import pyarrow as pa + import pyarrow.parquet as pa_parquet + + pa_parquet.write_table( + pa.table( + { + "id": [1, 2, 3], + "name": ["x", "y", "z"], + } + ), + parquet_source, + ) + imported_parquet = ICEBERG_BRIDGE.handle_import_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "imported_parquet", + "file_path": str(parquet_source), + "file_format": "parquet", + } + ) + self.assertTrue( + imported_parquet["ok"], imported_parquet.get("error") + ) + self.assertEqual(imported_parquet["row_count"], 3) + + preview = ICEBERG_BRIDGE.handle_preview_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "imported_parquet", + "limit": 10, + } + ) + self.assertTrue(preview["ok"]) + self.assertEqual(preview["total"], 3) + + def test_import_table_rejects_existing_table_and_missing_file(self) -> None: + """Existing tables and missing files fail with clean errors.""" + with tempfile.TemporaryDirectory() as catalog_directory: + created = ICEBERG_BRIDGE.handle_create_metadata_file( + {"warehouse_path": catalog_directory} + ) + self.assertTrue(created["ok"]) + + properties = { + "type": "sql", + "uri": f"sqlite:///{created['metadata_path']}", + "warehouse": Path(created["warehouse_path"]).as_uri(), + } + + source = Path(catalog_directory) / "source.csv" + source.write_text("id,name\n1,alpha\n", encoding="utf-8") + + first = ICEBERG_BRIDGE.handle_import_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "dup_table", + "file_path": str(source), + "file_format": "csv", + } + ) + self.assertTrue(first["ok"]) + + duplicate = ICEBERG_BRIDGE.handle_import_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "dup_table", + "file_path": str(source), + "file_format": "csv", + } + ) + self.assertFalse(duplicate["ok"]) + self.assertIn("already exists", duplicate["error"].lower()) + + missing = ICEBERG_BRIDGE.handle_import_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "missing_table", + "file_path": str(Path(catalog_directory) / "nope.csv"), + "file_format": "csv", + } + ) + self.assertFalse(missing["ok"]) + self.assertIn("file not found", missing["error"].lower()) + + def test_drop_and_rename_table(self) -> None: + """Drop and rename operations update the persisted catalog contract.""" + with tempfile.TemporaryDirectory() as catalog_directory: + created = ICEBERG_BRIDGE.handle_create_metadata_file( + {"warehouse_path": catalog_directory} + ) + self.assertTrue(created["ok"]) + + properties = { + "type": "sql", + "uri": f"sqlite:///{created['metadata_path']}", + "warehouse": Path(created["warehouse_path"]).as_uri(), + } + source = Path(catalog_directory) / "source.csv" + source.write_text("id,name\n1,alpha\n", encoding="utf-8") + + imported = ICEBERG_BRIDGE.handle_import_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "old_name", + "file_path": str(source), + "file_format": "csv", + } + ) + self.assertTrue(imported["ok"]) + + renamed = ICEBERG_BRIDGE.handle_rename_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "old_name", + "new_table": "new_name", + } + ) + self.assertTrue(renamed["ok"], renamed.get("error")) + self.assertEqual(renamed["table"], "new_name") + + tables_after_rename = ICEBERG_BRIDGE.handle_list_tables( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + } + ) + self.assertIn("new_name", tables_after_rename["tables"]) + self.assertNotIn("old_name", tables_after_rename["tables"]) + + dropped = ICEBERG_BRIDGE.handle_drop_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "new_name", + } + ) + self.assertTrue(dropped["ok"], dropped.get("error")) + + tables_after_drop = ICEBERG_BRIDGE.handle_list_tables( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + } + ) + self.assertEqual(tables_after_drop["tables"], []) + + def test_create_and_drop_namespace_lifecycle(self) -> None: + """Namespaces can be created (nested), listed, and dropped when empty.""" + with tempfile.TemporaryDirectory() as catalog_directory: + created = ICEBERG_BRIDGE.handle_create_metadata_file( + {"warehouse_path": catalog_directory} + ) + self.assertTrue(created["ok"]) + + properties = { + "type": "sql", + "uri": f"sqlite:///{created['metadata_path']}", + "warehouse": Path(created["warehouse_path"]).as_uri(), + } + + created_ns = ICEBERG_BRIDGE.handle_create_namespace( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["analytics", "daily"], + } + ) + self.assertTrue(created_ns["ok"], created_ns.get("error")) + self.assertEqual(created_ns["namespace"], ["analytics", "daily"]) + + namespaces = ICEBERG_BRIDGE.handle_list_namespaces( + { + "catalog_name": "local", + "catalog_properties": properties, + } + ) + self.assertTrue(namespaces["ok"]) + self.assertIn(["analytics"], namespaces["namespaces"]) + + nested = ICEBERG_BRIDGE.handle_list_namespaces( + { + "catalog_name": "local", + "catalog_properties": properties, + "parent": ["analytics"], + } + ) + self.assertTrue(nested["ok"]) + self.assertIn(["analytics", "daily"], nested["namespaces"]) + + duplicate = ICEBERG_BRIDGE.handle_create_namespace( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["analytics", "daily"], + } + ) + self.assertFalse(duplicate["ok"]) + self.assertIn("already exists", duplicate["error"].lower()) + + missing = ICEBERG_BRIDGE.handle_drop_namespace( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["does_not_exist"], + } + ) + self.assertFalse(missing["ok"]) + + # A namespace containing a table cannot be dropped. + source = Path(catalog_directory) / "source.csv" + source.write_text("id,name\n1,alpha\n", encoding="utf-8") + imported = ICEBERG_BRIDGE.handle_import_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["analytics", "daily"], + "table": "events", + "file_path": str(source), + "file_format": "csv", + } + ) + self.assertTrue(imported["ok"], imported.get("error")) + + non_empty = ICEBERG_BRIDGE.handle_drop_namespace( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["analytics", "daily"], + } + ) + self.assertFalse(non_empty["ok"]) + + dropped_table = ICEBERG_BRIDGE.handle_drop_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["analytics", "daily"], + "table": "events", + } + ) + self.assertTrue(dropped_table["ok"]) + + dropped_ns = ICEBERG_BRIDGE.handle_drop_namespace( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["analytics", "daily"], + } + ) + self.assertTrue(dropped_ns["ok"], dropped_ns.get("error")) + + namespaces_after = ICEBERG_BRIDGE.handle_list_namespaces( + { + "catalog_name": "local", + "catalog_properties": properties, + } + ) + self.assertTrue(namespaces_after["ok"]) + # Dropping the empty nested namespace also removes its now-empty + # parent in the SQL catalog. + self.assertNotIn(["analytics"], namespaces_after["namespaces"]) + + def test_drop_and_rename_reject_missing_tables(self) -> None: + """Dropping or renaming a non-existent table returns a clean error.""" + with tempfile.TemporaryDirectory() as catalog_directory: + created = ICEBERG_BRIDGE.handle_create_metadata_file( + {"warehouse_path": catalog_directory} + ) + self.assertTrue(created["ok"]) + + properties = { + "type": "sql", + "uri": f"sqlite:///{created['metadata_path']}", + "warehouse": Path(created["warehouse_path"]).as_uri(), + } + + dropped = ICEBERG_BRIDGE.handle_drop_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "ghost", + } + ) + self.assertFalse(dropped["ok"]) + + renamed = ICEBERG_BRIDGE.handle_rename_table( + { + "catalog_name": "local", + "catalog_properties": properties, + "namespace": ["default"], + "table": "ghost", + "new_table": "also_ghost", + } + ) + self.assertFalse(renamed["ok"]) + if __name__ == "__main__": unittest.main() diff --git a/tests/unit/main/services/icebergDatalake.service.test.ts b/tests/unit/main/services/icebergDatalake.service.test.ts index 88ff1009..4672c3ba 100644 --- a/tests/unit/main/services/icebergDatalake.service.test.ts +++ b/tests/unit/main/services/icebergDatalake.service.test.ts @@ -354,4 +354,161 @@ describe('IcebergDatalakeService compatibility and secret persistence', () => { }), ).toThrow('ICEBERG_WAREHOUSE_NOT_ALLOWED: hive/cloud'); }); + + describe('importTable validation', () => { + it('rejects unsupported file formats before Python executes', async () => { + await expect( + IcebergDatalakeService.importTable( + 'instance-1', + ['default'], + 't', + '/tmp/x.txt', + 'txt', + ), + ).rejects.toThrow('ICEBERG_IMPORT_FORMAT_UNSUPPORTED'); + }); + + it('rejects invalid table names', async () => { + await expect( + IcebergDatalakeService.importTable( + 'instance-1', + ['default'], + '1bad name!', + '/tmp/x.csv', + 'csv', + ), + ).rejects.toThrow('ICEBERG_IMPORT_TABLE_NAME_INVALID'); + }); + + it('rejects invalid or empty namespaces', async () => { + await expect( + IcebergDatalakeService.importTable( + 'instance-1', + [], + 'valid_table', + '/tmp/x.csv', + 'csv', + ), + ).rejects.toThrow('ICEBERG_NAMESPACE_INVALID'); + + await expect( + IcebergDatalakeService.importTable( + 'instance-1', + ['bad namespace!'], + 'valid_table', + '/tmp/x.csv', + 'csv', + ), + ).rejects.toThrow('ICEBERG_NAMESPACE_INVALID'); + }); + + it('rejects a missing file path', async () => { + await expect( + IcebergDatalakeService.importTable( + 'instance-1', + ['default'], + 'valid_table', + '', + 'csv', + ), + ).rejects.toThrow('ICEBERG_IMPORT_FILE_REQUIRED'); + }); + + it('rejects a non-existent source file before Python executes', async () => { + await expect( + IcebergDatalakeService.importTable( + 'instance-1', + ['default'], + 'valid_table', + '/definitely/not/a/real/file.csv', + 'csv', + ), + ).rejects.toThrow('ICEBERG_IMPORT_FILE_NOT_FOUND'); + }); + }); + + describe('createNamespace and dropNamespace validation', () => { + it('rejects invalid namespaces before Python executes', async () => { + await expect( + IcebergDatalakeService.createNamespace('instance-1', []), + ).rejects.toThrow('ICEBERG_NAMESPACE_INVALID'); + + await expect( + IcebergDatalakeService.createNamespace('instance-1', ['bad ns!']), + ).rejects.toThrow('ICEBERG_NAMESPACE_INVALID'); + + await expect( + IcebergDatalakeService.dropNamespace('instance-1', ['bad ns!']), + ).rejects.toThrow('ICEBERG_NAMESPACE_INVALID'); + }); + + it('passes sanitized nested namespaces to the bridge', async () => { + mockedLoadDatabase.mockResolvedValue({ + icebergInstances: [ + { + id: 'instance-1', + name: 'test', + catalogType: 'sqlite', + storageType: 'local', + localPath: '/tmp/warehouse', + createdAt: 'now', + updatedAt: 'now', + }, + ], + }); + const runBridgeSpy = jest + .spyOn(IcebergDatalakeService as any, 'runBridge') + .mockResolvedValue({ ok: true, namespace: ['a', 'b'] }); + + const result = await IcebergDatalakeService.createNamespace( + 'instance-1', + [' a ', 'b'], + ); + expect(result).toEqual({ namespace: ['a', 'b'] }); + expect(runBridgeSpy).toHaveBeenCalledTimes(1); + const command = runBridgeSpy.mock.calls[0][0] as Record; + expect(command.command).toBe('create_namespace'); + expect(command.namespace).toEqual(['a', 'b']); + + runBridgeSpy.mockRestore(); + }); + }); + + describe('dropTable and renameTable validation', () => { + it('rejects invalid table names before Python executes', async () => { + await expect( + IcebergDatalakeService.dropTable( + 'instance-1', + ['default'], + 'bad name!', + ), + ).rejects.toThrow('ICEBERG_TABLE_NAME_INVALID'); + + await expect( + IcebergDatalakeService.renameTable( + 'instance-1', + ['default'], + 'valid', + 'bad name!', + ), + ).rejects.toThrow('ICEBERG_TABLE_NAME_INVALID'); + }); + + it('rejects invalid namespaces before Python executes', async () => { + await expect( + IcebergDatalakeService.dropTable('instance-1', ['bad ns'], 'valid'), + ).rejects.toThrow('ICEBERG_NAMESPACE_INVALID'); + }); + + it('rejects renaming a table to its own name', async () => { + await expect( + IcebergDatalakeService.renameTable( + 'instance-1', + ['default'], + 'customers', + 'customers', + ), + ).rejects.toThrow('ICEBERG_RENAME_SAME_NAME'); + }); + }); }); From 3265dae1c487e71cce68e1caa9999b4d1a814bb4 Mon Sep 17 00:00:00 2001 From: Nuri Lacka Date: Tue, 11 Aug 2026 13:48:41 +0200 Subject: [PATCH 09/12] fix(datalake): normalize DuckLake instance timestamps - convert DuckLake created and updated dates to ISO strings - satisfy the unified DataLake table row contract - restore repository-wide TypeScript validation --- src/renderer/components/dataLake/DataLakeInstances.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/dataLake/DataLakeInstances.tsx b/src/renderer/components/dataLake/DataLakeInstances.tsx index 1cb0d14c..0aed842f 100644 --- a/src/renderer/components/dataLake/DataLakeInstances.tsx +++ b/src/renderer/components/dataLake/DataLakeInstances.tsx @@ -105,8 +105,8 @@ export const DataLakeInstances: React.FC = ({ lakeType: 'duck-lake', catalogType: instance.catalog.type.toUpperCase(), dataPath: instance.dataPath, - createdAt: instance.createdAt, - updatedAt: instance.updatedAt, + createdAt: new Date(instance.createdAt).toISOString(), + updatedAt: new Date(instance.updatedAt).toISOString(), })); const icebergRows: DataLakeTableRow[] = icebergInstances.map( From 253682b14987a46e0197858521042646dcd17192 Mon Sep 17 00:00:00 2001 From: Nuri Lacka Date: Wed, 12 Aug 2026 04:19:14 +0200 Subject: [PATCH 10/12] feat(datalake): refine Iceberg catalog and storage workflows - add catalog-specific icons across wizard, detail, and instance views - add cloud storage testing and bucket selection - preserve stored OAuth secrets when testing catalog connections - simplify managed catalog storage configuration - improve Iceberg import source selection - restore last visited route and table view state - remove unsupported catalog placeholders and DataLake query history UI - polish DataLake instance table actions and row icons --- assets/connectionIcons/catalog.png | Bin 0 -> 781 bytes assets/connectionIcons/hive.png | Bin 0 -> 81284 bytes assets/connectionIcons/index.ts | 17 + assets/connectionIcons/lakekeeper.png | Bin 0 -> 102700 bytes assets/connectionIcons/nessie.png | Bin 0 -> 16713 bytes assets/connectionIcons/polaris.png | Bin 0 -> 283351 bytes .../icebergDatalake.ipcHandlers.ts | 8 + src/main/services/icebergDatalake.service.ts | 152 ++++++-- src/renderer/App.tsx | 5 +- .../dataLake/DataLakeConnectionSelector.tsx | 98 +++++- .../components/dataLake/DataLakeDashboard.tsx | 102 ------ .../components/dataLake/DataLakeInstances.tsx | 84 ++++- .../components/dataLake/DataLakeSidebar.tsx | 39 +- .../dataLake/IcebergConnectionWizard.tsx | 333 +++++++++++++++--- .../dataLake/iceberg/IcebergDetail.tsx | 90 +++-- .../iceberg/IcebergTableImportWizard.tsx | 42 ++- .../components/dataLakeCards/index.tsx | 18 +- src/renderer/components/sidebar/index.tsx | 6 +- .../controllers/icebergDatalake.controller.ts | 18 +- src/renderer/hooks/useLastDataLakeRoute.ts | 43 +++ src/renderer/screens/dataLake/index.tsx | 25 +- src/renderer/services/iceberg.service.ts | 12 + src/types/frontend.ts | 2 + src/types/iceberg.ts | 15 +- src/types/ipc.ts | 2 + .../services/icebergDatalake.service.test.ts | 24 +- 26 files changed, 827 insertions(+), 308 deletions(-) create mode 100644 assets/connectionIcons/catalog.png create mode 100644 assets/connectionIcons/hive.png create mode 100644 assets/connectionIcons/lakekeeper.png create mode 100644 assets/connectionIcons/nessie.png create mode 100644 assets/connectionIcons/polaris.png create mode 100644 src/renderer/hooks/useLastDataLakeRoute.ts diff --git a/assets/connectionIcons/catalog.png b/assets/connectionIcons/catalog.png new file mode 100644 index 0000000000000000000000000000000000000000..2777db48aace6e50e8733dc198e556ece56e69b6 GIT binary patch literal 781 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

    eME)tl~R<7O1 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 v)*kxQe?=;Vx&CiU;!v~A%L8_pfBgZ9Mz; @@ -63,6 +68,18 @@ export const databaseIcons = { postgresql: postgres, }; +export const icebergCatalogImages = { + sqlite, + sql: postgres, + rest: catalog, + polaris, + lakekeeper, + nessie, + hive, +}; + +export const genericCatalogImage = catalog; + const obj: { images: Image } = { images: { snowflake, diff --git a/assets/connectionIcons/lakekeeper.png b/assets/connectionIcons/lakekeeper.png new file mode 100644 index 0000000000000000000000000000000000000000..3e417370020a1a98430220db47dc29eaaeee979f GIT binary patch literal 102700 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 C-*yuK literal 0 HcmV?d00001 diff --git a/assets/connectionIcons/nessie.png b/assets/connectionIcons/nessie.png new file mode 100644 index 0000000000000000000000000000000000000000..b090b526f59d5e0e553797d1c5a1b345af9cbbb8 GIT binary patch literal 16713 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 P00000NkvXXu0mjf)J)dW literal 0 HcmV?d00001 diff --git a/assets/connectionIcons/polaris.png b/assets/connectionIcons/polaris.png new file mode 100644 index 0000000000000000000000000000000000000000..6573f7a61bbfa4431720e9179297c3eed71dd6c9 GIT binary patch literal 283351 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

    6TLb3Fh^@LT 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 z

    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�f9md(U!J6cGNUg1#ct@ZW+X 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

    dZ(@Cu}k&M%G8EM7=Ymx)i@4>Q1gr8L;j0q2C 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 P0SG)@{an^LB{Ts5)YlGU literal 0 HcmV?d00001 diff --git a/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts b/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts index 4ede448a..a6ae3334 100644 --- a/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts +++ b/src/main/ipcHandlers/icebergDatalake.ipcHandlers.ts @@ -34,6 +34,14 @@ export const registerIcebergDatalakeHandlers = () => { IcebergDatalakeService.testCatalogConnection(params), ); + ipcMain.handle('iceberg:testStorage', (_e, params) => + IcebergDatalakeService.testStorageConnection(params), + ); + + ipcMain.handle('iceberg:listStorageBuckets', (_e, params) => + IcebergDatalakeService.listStorageBuckets(params), + ); + ipcMain.handle('iceberg:testInstance', (_e, id: string) => IcebergDatalakeService.testInstanceConnection(id), ); diff --git a/src/main/services/icebergDatalake.service.ts b/src/main/services/icebergDatalake.service.ts index 28234a0a..f81f26cb 100644 --- a/src/main/services/icebergDatalake.service.ts +++ b/src/main/services/icebergDatalake.service.ts @@ -24,6 +24,8 @@ import type { CreateIcebergInstanceDTO, UpdateIcebergInstanceDTO, IcebergTestCatalogParams, + IcebergTestStorageParams, + IcebergListStorageBucketsParams, IcebergTestResult, IcebergFieldSpec, IcebergSchemaResult, @@ -37,7 +39,7 @@ import type { IcebergTableOperationResult, IcebergNamespaceOperationResult, } from '../../types/iceberg'; -import type { CloudConnection } from '../../types/frontend'; +import type { CloudConnection, CloudStorageConfig } from '../../types/frontend'; import type { PostgresConnection } from '../../types/backend'; export class IcebergDatalakeService { @@ -116,25 +118,6 @@ export class IcebergDatalakeService { authModes: ['none'], allowedStorageTypes: ['local'], }, - ...( - [ - ['glue', 'AWS Glue', 'glue'], - ['biglake', 'Google BigLake', 'custom'], - ['onelake', 'Microsoft Fabric OneLake', 'custom'], - ['unity', 'Databricks Unity Catalog', 'custom'], - ['snowflake', 'Snowflake Iceberg Catalog', 'custom'], - ['cloudflare', 'Cloudflare R2 Data Catalog', 'custom'], - ] as const - ).map(([type, label, pyicebergType]) => ({ - type, - label, - pyicebergType, - enabled: false, - disabledReason: 'Available by request through a GitHub issue.', - requiredFields: [], - authModes: ['none'] as IcebergCatalogCapability['authModes'], - allowedStorageTypes: [], - })), ]; // In-process cache: once we confirm pyiceberg is installed for this app @@ -782,7 +765,7 @@ export class IcebergDatalakeService { const id = uuidv4(); const now = new Date().toISOString(); - let catalogAccessTokenKey: string | undefined; + let catalogAccessTokenKey: `iceberg-catalog-token-${string}` | undefined; if (data.accessToken) { catalogAccessTokenKey = `iceberg-catalog-token-${id}`; await secureStorage.setCredential( @@ -791,7 +774,7 @@ export class IcebergDatalakeService { ); } - let oauthClientSecretKey: string | undefined; + let oauthClientSecretKey: `iceberg-oauth-secret-${string}` | undefined; if (data.oauthClientSecret) { oauthClientSecretKey = `iceberg-oauth-secret-${id}`; await secureStorage.setCredential( @@ -935,6 +918,30 @@ export class IcebergDatalakeService { try { const props: Record = {}; const env: Record = {}; + let { oauthClientSecret } = params; + if ( + params.authMode === 'oauth-client-credentials' && + !oauthClientSecret && + params.instanceId + ) { + const existingInstance = await IcebergDatalakeService.getInstance( + params.instanceId, + ); + const matchesSavedAuthentication = + existingInstance.catalogType === params.catalogType && + existingInstance.endpoint === params.endpoint && + existingInstance.oauthClientId === params.oauthClientId && + existingInstance.oauthServerUri === params.oauthServerUri; + if (!matchesSavedAuthentication) { + throw new Error('ICEBERG_OAUTH_CLIENT_SECRET_REQUIRED'); + } + if (existingInstance.oauthClientSecretKey) { + oauthClientSecret = + (await secureStorage.getCredential( + existingInstance.oauthClientSecretKey, + )) ?? undefined; + } + } const storageType = params.storageType ?? @@ -951,7 +958,7 @@ export class IcebergDatalakeService { catalogAuthMode: params.authMode, accessToken: params.accessToken, oauthClientId: params.oauthClientId, - oauthClientSecret: params.oauthClientSecret, + oauthClientSecret, oauthServerUri: params.oauthServerUri, }); @@ -997,11 +1004,11 @@ export class IcebergDatalakeService { env['ICEBERG_ACCESS_TOKEN'] = params.accessToken; } if (params.authMode === 'oauth-client-credentials') { - if (!params.oauthClientId || !params.oauthClientSecret) { + if (!params.oauthClientId || !oauthClientSecret) { throw new Error('ICEBERG_OAUTH_CLIENT_CREDENTIALS_REQUIRED'); } props.credential = '__ENV:ICEBERG_OAUTH_CREDENTIAL'; - env.ICEBERG_OAUTH_CREDENTIAL = `${params.oauthClientId}:${params.oauthClientSecret}`; + env.ICEBERG_OAUTH_CREDENTIAL = `${params.oauthClientId}:${oauthClientSecret}`; if (params.oauthServerUri) { props['oauth2-server-uri'] = params.oauthServerUri; } @@ -1057,6 +1064,101 @@ export class IcebergDatalakeService { } } + static async testStorageConnection( + params: IcebergTestStorageParams, + ): Promise { + try { + const connectionId = params.connectionId?.trim(); + const bucket = params.bucket?.trim(); + if (!connectionId || !bucket) { + throw new Error('ICEBERG_REQUIRED_FIELD: connectionId/bucket'); + } + + const { connection, config } = + await IcebergDatalakeService.resolveCloudStorageConnection( + connectionId, + ); + + const CloudExplorerService = (await import('./cloudExplorer.service')) + .default; + await CloudExplorerService.listObjects( + connection.provider, + config as unknown as CloudStorageConfig, + bucket, + undefined, + params.prefix?.trim() ?? '', + ); + return { + success: true, + warehouseConnected: true, + checkedAt: new Date().toISOString(), + }; + } catch (error) { + // eslint-disable-next-line no-console + console.error( + '[IcebergDatalakeService] testStorageConnection error:', + error, + ); + return { + success: false, + warehouseConnected: false, + checkedAt: new Date().toISOString(), + error: error instanceof Error ? error.message : String(error), + }; + } + } + + static async listStorageBuckets( + params: IcebergListStorageBucketsParams, + ): Promise { + const connectionId = params.connectionId?.trim(); + if (!connectionId) { + throw new Error('ICEBERG_REQUIRED_FIELD: connectionId'); + } + const { connection, config } = + await IcebergDatalakeService.resolveCloudStorageConnection(connectionId); + const CloudExplorerService = (await import('./cloudExplorer.service')) + .default; + const buckets = await CloudExplorerService.listBuckets( + connection.provider, + config as unknown as CloudStorageConfig, + ); + return buckets.map((bucket) => bucket.name); + } + + private static async resolveCloudStorageConnection(connectionId: string) { + const db = await loadDatabaseFile(); + const connection = (db.sources ?? []).find( + (source) => source.id === connectionId, + ); + if (!connection) throw new Error('ICEBERG_CLOUD_CONNECTION_NOT_FOUND'); + if ( + !IcebergDatalakeService.cloudProviders.includes( + connection.provider as (typeof IcebergDatalakeService.cloudProviders)[number], + ) + ) { + throw new Error('ICEBERG_CLOUD_PROVIDER_NOT_SUPPORTED'); + } + + const config = { ...connection.config } as Record; + const secret = await secureStorage.getCredential( + `cloud-${connection.provider}-${connection.id}`, + ); + if (connection.provider === 'azure') config.accountKey = secret; + else if (connection.provider === 'gcs') config.credentials = secret; + else if (connection.provider === 'backblaze-b2') { + config.applicationKey = secret; + } else config.secretAccessKey = secret; + + if (connection.provider === 'aws') { + const sessionToken = await secureStorage.getCredential( + `cloud-aws-session-${connection.id}`, + ); + if (sessionToken) config.sessionToken = sessionToken; + } + return { connection, config }; + } + static async testInstanceConnection(id: string): Promise { try { const instance = await IcebergDatalakeService.getInstance(id); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 9373ec0c..191c28da 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -104,7 +104,10 @@ const App: React.FC = () => { } /> } /> } /> - } /> + } + /> } diff --git a/src/renderer/components/dataLake/DataLakeConnectionSelector.tsx b/src/renderer/components/dataLake/DataLakeConnectionSelector.tsx index dc7cd3b1..4174b75e 100644 --- a/src/renderer/components/dataLake/DataLakeConnectionSelector.tsx +++ b/src/renderer/components/dataLake/DataLakeConnectionSelector.tsx @@ -53,6 +53,7 @@ interface DataLakeConnectionSelectorProps { initialConnectionId?: string; initialBucket?: string; initialPrefix?: string; + loadBuckets?: (connectionId: string) => Promise; } type InlineProvider = 'aws' | 'azure' | 'gcs'; @@ -108,6 +109,7 @@ export const DataLakeConnectionSelector: React.FC< initialConnectionId, initialBucket, initialPrefix, + loadBuckets, }) => { const { data: connections, isLoading, refetch } = useCloudConnections(); const createConnection = useCreateCloudConnection(); @@ -131,6 +133,9 @@ export const DataLakeConnectionSelector: React.FC< const [selectedConnectionId, setSelectedConnectionId] = useState(''); const [bucket, setBucket] = useState(''); const [prefix, setPrefix] = useState(''); + const [bucketOptions, setBucketOptions] = useState([]); + const [isLoadingBuckets, setIsLoadingBuckets] = useState(false); + const [bucketLoadError, setBucketLoadError] = useState(null); // Modal state const [isModalOpen, setIsModalOpen] = useState(false); @@ -140,7 +145,7 @@ export const DataLakeConnectionSelector: React.FC< // Update parent when connection or bucket changes React.useEffect(() => { - if (selectedConnectionId && bucket) { + if (selectedConnectionId) { const selectedConnection = connections?.find( (connection: CloudConnection) => connection.id === selectedConnectionId, ); @@ -153,6 +158,32 @@ export const DataLakeConnectionSelector: React.FC< } }, [selectedConnectionId, bucket, prefix, connections, onSelectExisting]); + useEffect(() => { + if (!selectedConnectionId || !loadBuckets) return undefined; + let cancelled = false; + setIsLoadingBuckets(true); + setBucketLoadError(null); + // eslint-disable-next-line no-void + void (async () => { + try { + const names = await loadBuckets(selectedConnectionId); + if (!cancelled) setBucketOptions(names); + } catch (error: unknown) { + if (!cancelled) { + setBucketOptions([]); + setBucketLoadError( + error instanceof Error ? error.message : 'Failed to load buckets.', + ); + } + } finally { + if (!cancelled) setIsLoadingBuckets(false); + } + })(); + return () => { + cancelled = true; + }; + }, [selectedConnectionId, loadBuckets]); + // Sync initial values from parent when revisiting the step useEffect(() => { if (initialConnectionId && initialConnectionId !== selectedConnectionId) { @@ -601,7 +632,13 @@ export const DataLakeConnectionSelector: React.FC< Cloud Connection setBucket(event.target.value)} + disabled={!selectedConnectionId || isLoadingBuckets} + > + {isLoadingBuckets && Loading buckets…} + {!isLoadingBuckets && bucketOptions.length === 0 && ( + No buckets found + )} + {bucketOptions.map((name) => ( + + {name} + + ))} + + + {getBucketHelperText()} + + + ) : ( + setBucket(e.target.value)} + required + helperText={getBucketHelperText()} + disabled={!selectedConnectionId} + /> + )} + {bucketLoadError && {bucketLoadError}} {/* Prefix Input */} setIsModalOpen(false)} - maxWidth={supportsInlineCreate ? 'sm' : 'lg'} + maxWidth="sm" fullWidth + PaperProps={{ + sx: !supportsInlineCreate + ? { bgcolor: 'transparent', boxShadow: 'none' } + : undefined, + }} > {!supportsInlineCreate ? ( diff --git a/src/renderer/components/dataLake/DataLakeDashboard.tsx b/src/renderer/components/dataLake/DataLakeDashboard.tsx index 70f084f6..94a101f9 100644 --- a/src/renderer/components/dataLake/DataLakeDashboard.tsx +++ b/src/renderer/components/dataLake/DataLakeDashboard.tsx @@ -12,7 +12,6 @@ import { import { Storage, TableChart, - QueryStats, Settings, Dashboard, Add, @@ -380,47 +379,6 @@ export const DataLakeDashboard: React.FC = ({ - - - - Recent Queries - - - - } - sx={{ pb: 1 }} - /> - - - 0 - - - Coming soon - - - - = ({ - - - - - - Recent Queries - - - Coming soon - - - - - } - /> - - - Coming Soon - - - Query history tracking is not yet implemented - - - {stats.totalInstances === 0 && ( diff --git a/src/renderer/components/dataLake/DataLakeInstances.tsx b/src/renderer/components/dataLake/DataLakeInstances.tsx index 0aed842f..87de2478 100644 --- a/src/renderer/components/dataLake/DataLakeInstances.tsx +++ b/src/renderer/components/dataLake/DataLakeInstances.tsx @@ -17,7 +17,7 @@ import { CircularProgress, Alert, } from '@mui/material'; -import { Edit, Delete, Storage, Refresh } from '@mui/icons-material'; +import { Edit, Delete, Folder, Storage, Refresh } from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; import moment from 'moment'; import { @@ -31,6 +31,13 @@ import { import { ConfirmationModal } from '../modals/confirmationModal'; import type { IcebergInstanceListItem } from '../../../types/iceberg'; import { IcebergIcon } from './iceberg/IcebergIcon'; +import { + cloudStorageImages, + databaseIcons, + genericCatalogImage, + icebergCatalogImages, +} from '../../../../assets/connectionIcons'; +import { icons } from '../../../../assets/icons'; type DataLakeTableRow = | { @@ -55,15 +62,45 @@ type DataLakeTableRow = icebergInstance: IcebergInstanceListItem; }; +const getCatalogIcon = (row: DataLakeTableRow) => { + if (row.lakeType === 'duck-lake') return databaseIcons.duckdb; + return ( + icebergCatalogImages[ + row.catalogType.toLowerCase() as keyof typeof icebergCatalogImages + ] ?? genericCatalogImage + ); +}; + +const getStorageIcon = (row: DataLakeTableRow) => { + const path = row.dataPath.toLowerCase(); + let icon; + if (!icon && path.startsWith('s3://')) icon = cloudStorageImages.s3; + if (!icon && path.startsWith('gs://')) icon = cloudStorageImages.gcs; + if (!icon && path.startsWith('abfss://')) icon = cloudStorageImages.azure; + + if (icon) { + return ( + + ); + } + if (path === 'server-managed') { + return ; + } + return ; +}; + interface DataLakeInstancesProps { onInstanceSelect?: (instanceId: string) => void; - onAddIceberg?: () => void; onEditIceberg?: (instanceId: string) => void; } export const DataLakeInstances: React.FC = ({ onInstanceSelect, - onAddIceberg, onEditIceberg, }) => { const navigate = useNavigate(); @@ -229,16 +266,6 @@ export const DataLakeInstances: React.FC = ({ - {onAddIceberg && ( - - )} = ({ {row.lakeType === 'duck-lake' ? ( + } label="DuckLake" size="small" color="primary" @@ -327,6 +362,14 @@ export const DataLakeInstances: React.FC = ({ + } label={row.catalogType} size="small" variant="outlined" @@ -335,12 +378,15 @@ export const DataLakeInstances: React.FC = ({ - - {row.dataPath} - + + {getStorageIcon(row)} + + {row.dataPath} + + diff --git a/src/renderer/components/dataLake/DataLakeSidebar.tsx b/src/renderer/components/dataLake/DataLakeSidebar.tsx index 51546b67..42d18621 100644 --- a/src/renderer/components/dataLake/DataLakeSidebar.tsx +++ b/src/renderer/components/dataLake/DataLakeSidebar.tsx @@ -9,8 +9,9 @@ import { ListItemText, styled, Button, + Chip, } from '@mui/material'; -import { Dashboard, History, Add } from '@mui/icons-material'; +import { Dashboard, Add } from '@mui/icons-material'; import { NavLink, useLocation, useNavigate } from 'react-router-dom'; import { SettingsSidebarElement } from '../../screens/settings/settingsElements'; import { icons } from '../../../../assets'; @@ -67,11 +68,6 @@ export const dataLakeSidebarElements: SettingsSidebarElement[] = [ text: 'DataLakes', path: '/app/data-lake/instances', }, - { - icon: History, - text: 'Query History', - path: '/app/data-lake/history', - }, ]; interface DataLakeSidebarProps { @@ -239,20 +235,33 @@ export const DataLakeSidebar: React.FC = ({ {/* Iceberg Instances */} {icebergInstances.length > 0 && ( - - Apache Iceberg - + + Apache Iceberg + + + (null); + const [storageTestResult, setStorageTestResult] = useState<{ + success: boolean; + message: string; + } | null>(null); useEffect(() => { if (initialData) { @@ -278,11 +286,80 @@ export const IcebergConnectionWizard: React.FC< setActiveStep(0); setStepError(null); setCatalogTestResult(null); + setStorageTestResult(null); } }, [initialData]); + useEffect(() => { + const secretKey = initialData?.oauthClientSecretKey; + if (mode !== 'edit' || !secretKey) return undefined; + + let cancelled = false; + // eslint-disable-next-line no-void + void (async () => { + try { + const savedSecret = await secureStorageService.get(secretKey); + if (!cancelled && savedSecret) { + setData((current) => ({ + ...current, + catalog: { ...current.catalog, oauthClientSecret: savedSecret }, + })); + } + } catch (error) { + if (!cancelled) { + setStepError( + error instanceof Error + ? error.message + : 'Failed to load the saved OAuth secret.', + ); + } + } + })(); + + return () => { + cancelled = true; + }; + }, [initialData?.oauthClientSecretKey, mode]); + + useEffect(() => { + if ( + data.catalog.catalogType !== 'polaris' && + data.catalog.catalogType !== 'lakekeeper' + ) { + return; + } + if ( + !data.catalog.polarisConnectionId && + !data.catalog.polarisBucket && + !data.catalog.polarisPrefix + ) { + return; + } + setData((current) => ({ + ...current, + catalog: { + ...current.catalog, + polarisConnectionId: undefined, + polarisBucket: undefined, + polarisPrefix: undefined, + }, + })); + }, [ + data.catalog.catalogType, + data.catalog.polarisBucket, + data.catalog.polarisConnectionId, + data.catalog.polarisPrefix, + ]); + // Catalog test const testCatalogMutation = useTestIcebergCatalog(); + const testStorageMutation = useTestIcebergStorage(); + const listStorageBucketsMutation = useListIcebergStorageBuckets(); + const loadStorageBuckets = useCallback( + (connectionId: string) => + listStorageBucketsMutation.mutateAsync({ connectionId }), + [listStorageBucketsMutation.mutateAsync], + ); const createLocalCatalogMutation = useCreateIcebergMetadataFile(); const capabilitiesQuery = useIcebergCapabilities(); const catalogCapabilities = capabilitiesQuery.data?.catalogs ?? []; @@ -357,6 +434,43 @@ export const IcebergConnectionWizard: React.FC< const patchStorage = (patch: Partial) => setData((d) => ({ ...d, storage: { ...d.storage, ...patch } })); + const handleSelectVendedStorage = useCallback( + (connectionId: string, bucket: string, prefix?: string) => { + setData((current) => ({ + ...current, + catalog: { + ...current.catalog, + polarisConnectionId: connectionId, + polarisBucket: bucket, + polarisPrefix: prefix, + }, + })); + }, + [], + ); + + const handleSelectCloudStorage = useCallback( + ( + connectionId: string, + bucket: string, + prefix?: string, + provider?: IcebergCloudProvider, + ) => { + setStorageTestResult(null); + setData((current) => ({ + ...current, + storage: { + ...current.storage, + connectionId, + bucket, + prefix, + cloudProvider: provider, + }, + })); + }, + [], + ); + const initializeLocalCatalog = (catalogDirectory: string) => { setStepError(null); createLocalCatalogMutation.mutate(catalogDirectory, { @@ -391,6 +505,7 @@ export const IcebergConnectionWizard: React.FC< setCatalogTestResult(null); try { const result = await testCatalogMutation.mutateAsync({ + instanceId: mode === 'edit' ? initialData?.id : undefined, catalogType: data.catalog.catalogType, catalogPath: data.catalog.catalogPath, endpoint: data.catalog.endpoint, @@ -425,6 +540,35 @@ export const IcebergConnectionWizard: React.FC< } }; + const handleTestStorage = async () => { + setStorageTestResult(null); + const validationError = validateStep(2, data); + if (validationError) { + setStorageTestResult({ success: false, message: validationError }); + return; + } + try { + const result = await testStorageMutation.mutateAsync({ + connectionId: data.storage.connectionId!, + bucket: data.storage.bucket!, + prefix: data.storage.prefix, + }); + setStorageTestResult({ + success: result.success, + message: result.success + ? 'Cloud object storage is accessible.' + : (result.error ?? 'Cloud storage test failed.'), + }); + } catch (error: any) { + // eslint-disable-next-line no-console + console.error(error); + setStorageTestResult({ + success: false, + message: error?.message ?? 'Cloud storage test failed.', + }); + } + }; + // ── Step content renderers ────────────────────────────────────────────── const renderBasicsStep = () => ( @@ -466,6 +610,30 @@ export const IcebergConnectionWizard: React.FC< @@ -764,7 +947,7 @@ export const IcebergConnectionWizard: React.FC< } helperText={ mode === 'edit' && initialData?.oauthClientSecretKey - ? 'Leave blank to keep the existing secret.' + ? 'Loaded securely from the system keychain.' : 'Stored securely in the system keychain.' } InputProps={{ @@ -846,43 +1029,55 @@ export const IcebergConnectionWizard: React.FC< Storage Configuration - {data.catalog.catalogType === 'nessie' ? ( + {data.catalog.catalogType === 'polaris' && ( + + Storage is managed by Polaris for this catalog. Polaris provides + the authoritative warehouse location and object-store access, so + no Cloud Explorer connection is required. + + )} + {data.catalog.catalogType === 'nessie' && ( Nessie manages the warehouse and sends the required FileIO and object-store configuration to PyIceberg. DBT Studio uses remote request signing, so no Cloud Explorer credentials are required. - ) : ( - <> - - REST catalogs manage table storage on the server. Configure a - cloud connection below only if your catalog uses{' '} - vended credentials to delegate access to object - storage. - - - Vended credentials (optional) - - - Select a Cloud Explorer connection and bucket the catalog can - use when delegating storage access. Leave blank if credentials - are handled another way. - - - patchCatalog({ - polarisConnectionId: connectionId, - polarisBucket: bucket, - polarisPrefix: prefix, - }) - } - initialConnectionId={data.catalog.polarisConnectionId} - initialBucket={data.catalog.polarisBucket} - initialPrefix={data.catalog.polarisPrefix} - /> - )} + {data.catalog.catalogType === 'lakekeeper' && ( + + Storage is managed by the selected Lakekeeper warehouse. Configure + its bucket, prefix, endpoint, and credentials in the Lakekeeper + UI. DBT Studio uses remote request signing, so no Cloud Explorer + connection is required. + + )} + {data.catalog.catalogType !== 'polaris' && + data.catalog.catalogType !== 'lakekeeper' && + data.catalog.catalogType !== 'nessie' && ( + <> + + REST catalogs manage table storage on the server. Configure a + cloud connection below only if your catalog uses{' '} + vended credentials to delegate access to + object storage. + + + Vended credentials (optional) + + + Select a Cloud Explorer connection and bucket the catalog can + use when delegating storage access. Leave blank if credentials + are handled another way. + + + + )} ); } @@ -952,20 +1147,44 @@ export const IcebergConnectionWizard: React.FC< )} {data.storage.storageType === 'cloud' && ( - - patchStorage({ - connectionId, - bucket, - prefix, - cloudProvider: provider, - }) - } - initialConnectionId={data.storage.connectionId} - initialBucket={data.storage.bucket} - initialPrefix={data.storage.prefix} - /> + <> + + + + {storageTestResult && ( + : undefined} + > + {storageTestResult.message} + + )} + + )} ); diff --git a/src/renderer/components/dataLake/iceberg/IcebergDetail.tsx b/src/renderer/components/dataLake/iceberg/IcebergDetail.tsx index a45d2462..741fe83a 100644 --- a/src/renderer/components/dataLake/iceberg/IcebergDetail.tsx +++ b/src/renderer/components/dataLake/iceberg/IcebergDetail.tsx @@ -60,12 +60,13 @@ import { TableChart, } from '@mui/icons-material'; import { useQueryClient } from 'react-query'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { toast } from 'react-toastify'; import moment from 'moment'; import { cloudStorageImages, - databaseIcons, + genericCatalogImage, + icebergCatalogImages, } from '../../../../../assets/connectionIcons'; import type { IcebergImportFileFormat, @@ -453,10 +454,20 @@ const TableDetail: React.FC<{ selection: SelectedTable; }> = ({ instance, selection }) => { const navigate = useNavigate(); - const [tab, setTab] = React.useState(0); - const [limit, setLimit] = React.useState(100); - const [rowFilter, setRowFilter] = React.useState(''); - const [appliedFilter, setAppliedFilter] = React.useState(''); + const [searchParams, setSearchParams] = useSearchParams(); + const requestedTab = Number(searchParams.get('tab')); + const initialTab = Number.isInteger(requestedTab) + ? Math.min(Math.max(requestedTab, 0), 4) + : 0; + const requestedLimit = Number(searchParams.get('limit')); + const initialLimit = [50, 100, 500, 1000].includes(requestedLimit) + ? requestedLimit + : 100; + const initialFilter = searchParams.get('filter') ?? ''; + const [tab, setTab] = React.useState(initialTab); + const [limit, setLimit] = React.useState(initialLimit); + const [rowFilter, setRowFilter] = React.useState(initialFilter); + const [appliedFilter, setAppliedFilter] = React.useState(initialFilter); const [filterError, setFilterError] = React.useState(null); const schemaQuery = useGetIcebergSchema( instance.id, @@ -477,6 +488,22 @@ const TableDetail: React.FC<{ tab === 2, ); + const updateTableViewParams = React.useCallback( + (updates: { tab?: number; limit?: number; filter?: string }) => { + const next = new URLSearchParams(searchParams); + if (updates.tab !== undefined) next.set('tab', String(updates.tab)); + if (updates.limit !== undefined) { + next.set('limit', String(updates.limit)); + } + if (updates.filter !== undefined) { + if (updates.filter) next.set('filter', updates.filter); + else next.delete('filter'); + } + setSearchParams(next, { replace: true }); + }, + [searchParams, setSearchParams], + ); + const applyFilter = () => { const value = rowFilter.trim(); if (value && !/^[A-Za-z0-9_\s.'"<>=!()-]+$/.test(value)) { @@ -485,6 +512,7 @@ const TableDetail: React.FC<{ } setFilterError(null); setAppliedFilter(value); + updateTableViewParams({ filter: value }); if (value === appliedFilter) previewQuery.refetch(); }; @@ -679,7 +707,11 @@ const TableDetail: React.FC<{