Click a collection in the tree to get started
Type the collection name to confirm:', input: 'text', inputPlaceholder: collName, confirmButtonText: 'Drop Collection', icon: 'warning', didOpen: function() { var container = document.querySelector('.swal2-container'); if (!container) return; var input = container.querySelector('.swal2-input'); var confirmBtn = container.querySelector('.swal2-confirm'); if (!input || !confirmBtn) return; confirmBtn.disabled = true; input.addEventListener('input', function() { confirmBtn.disabled = (input.value !== collName); }); }, inputValidator: function(value) { if (value !== collName) return 'Name does not match'; } }); if (!result) return; try { var body = new URLSearchParams(); body.append('database_cluster', connName); body.append('database', dbName); body.append('script', 'ctx.DB.RunCommand("' + connName + '", "' + dbName + '", {"drop": "' + collName + '"}); return {dropped: "' + collName + '"};'); var response = await fetch('/api/execute-script', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body }); if (response.ok) { wc.Prompt.toast({title: 'Collection "' + collName + '" dropped', icon: 'success'}); // Surgical: remove just this collection's tree node. var tree = document.getElementById('ds-tree'); var connItem = tree && tree.querySelector('wc-tree-item[icon="server"][label="' + CSS.escape(connName) + '"]'); var dbItem = connItem && connItem.querySelector('wc-tree-item[icon="database"][label="' + CSS.escape(dbName) + '"]'); var collItem = dbItem && dbItem.querySelector('wc-tree-item[icon="table"][label="' + CSS.escape(collName) + '"]'); if (collItem) collItem.remove(); } else { var errText = await response.text(); wc.Prompt.toast({title: 'Error: ' + errText, icon: 'error'}); } } catch(e) { wc.Prompt.toast({title: 'Error: ' + e.message, icon: 'error'}); } }; window.dsClearCollection = async function(connName, dbName, collName) { if (window.dsDemoGuard && window.dsDemoGuard()) return; var result = await wc.Prompt.fire({ title: 'Clear Collection', html: 'This will delete all documents from ' + collName + '. ' + 'Indexes and the collection itself remain.
Type the collection name to confirm:', input: 'text', inputPlaceholder: collName, confirmButtonText: 'Clear Collection', icon: 'warning', inputValidator: function(value) { if (value !== collName) return 'Name does not match'; } }); if (!result) return; try { var body = new URLSearchParams(); body.append('database_cluster', connName); body.append('database', dbName); body.append('script', 'var r = ctx.DB.DeleteMany(' + JSON.stringify(connName) + ', ' + JSON.stringify(dbName) + ', ' + JSON.stringify(collName) + ', {}); ' + 'return {cleared: ' + JSON.stringify(collName) + ', deleted: r};' ); var response = await fetch('/api/execute-script', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body }); if (response.ok) { wc.Prompt.toast({title: 'Collection "' + collName + '" cleared', icon: 'success'}); if (window.dsRefreshDatabaseNode) window.dsRefreshDatabaseNode(connName, dbName); } else { var errText = await response.text(); wc.Prompt.toast({title: 'Error: ' + errText, icon: 'error'}); } } catch(e) { wc.Prompt.toast({title: 'Error: ' + e.message, icon: 'error'}); } }; window.dsDropDatabase = async function(connName, dbName) { if (window.dsDemoGuard && window.dsDemoGuard()) return; var result = await wc.Prompt.fire({ title: 'Drop Database', html: 'This will permanently delete ' + dbName + ' and ALL its collections and data.
Type the database name to confirm:', input: 'text', inputPlaceholder: dbName, confirmButtonText: 'Drop Database', icon: 'warning', didOpen: function() { var container = document.querySelector('.swal2-container'); if (!container) return; var input = container.querySelector('.swal2-input'); var confirmBtn = container.querySelector('.swal2-confirm'); if (!input || !confirmBtn) return; confirmBtn.disabled = true; input.addEventListener('input', function() { confirmBtn.disabled = (input.value !== dbName); }); }, inputValidator: function(value) { if (value !== dbName) return 'Name does not match'; } }); if (!result) return; try { var body = new URLSearchParams(); body.append('database_cluster', connName); body.append('database', dbName); body.append('script', 'ctx.DB.RunCommand("' + connName + '", "' + dbName + '", {"dropDatabase": 1}); return {dropped: "' + dbName + '"};'); var response = await fetch('/api/execute-script', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body }); if (response.ok) { wc.Prompt.toast({title: 'Database "' + dbName + '" dropped', icon: 'success'}); // Surgical: remove just this database's tree node. No full refresh, // no collapse of the rest of the tree. var tree = document.getElementById('ds-tree'); var connItem = tree && tree.querySelector('wc-tree-item[icon="server"][label="' + CSS.escape(connName) + '"]'); var dbItem = connItem && connItem.querySelector('wc-tree-item[icon="database"][label="' + CSS.escape(dbName) + '"]'); if (dbItem) dbItem.remove(); } else { var errText = await response.text(); wc.Prompt.toast({title: 'Error: ' + errText, icon: 'error'}); } } catch(e) { wc.Prompt.toast({title: 'Error: ' + e.message, icon: 'error'}); } }; window.dsOpenAddDatabaseModal = function(connName) { var url = '/x/data_studio_add_database/create' + '?targetConn=' + encodeURIComponent(connName); htmx.ajax('GET', url, { target: '#modal-container', swap: 'innerHTML', indicator: '#content-loader', pushUrl: false }); }; window.dsAddCollection = async function(connName, dbName) { if (window.dsDemoGuard && window.dsDemoGuard()) return; var result = await wc.Prompt.fire({ title: 'Add Collection', html: 'Enter a name for the new collection in ' + dbName + ':', input: 'text', inputPlaceholder: 'collection_name', confirmButtonText: 'Create', icon: 'question', inputValidator: function(value) { var v = (value || '').trim(); if (!v) return 'Collection name is required'; if (v.length > 100) return 'Maximum 100 characters'; if (v.charAt(0) === '$') return 'Cannot start with $'; if (/\0/.test(v)) return 'Cannot contain null character'; if (/^system\./.test(v)) return 'Cannot start with "system."'; } }); if (!result || !result.value) return; var collName = result.value.trim(); try { var script = 'ctx.DB.EnsureCollection(' + JSON.stringify(connName) + ', ' + JSON.stringify(dbName) + ', ' + JSON.stringify(collName) + ');' + ' return {created: ' + JSON.stringify(dbName + '.' + collName) + '};'; var body = new URLSearchParams(); body.append('database_cluster', connName); body.append('database', dbName); body.append('script', script); var response = await fetch('/api/execute-script', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body }); if (response.ok) { wc.Prompt.toast({title: 'Collection "' + collName + '" created', icon: 'success'}); if (window.dsRefreshDatabaseNode) window.dsRefreshDatabaseNode(connName, dbName); } else { var errText = await response.text(); wc.Prompt.toast({title: 'Error: ' + errText, icon: 'error'}); } } catch(e) { wc.Prompt.toast({title: 'Error: ' + e.message, icon: 'error'}); } }; // ========== Open Database Overview ========== window.dsOpenDatabaseOverview = function(connName, dbName) { var label = dbName; var toolId = 'db_' + connName + '_' + dbName; if (_openTabs[toolId]) { var wsTab = document.getElementById('ds-workspace'); var btn = wsTab.querySelector('button.tab-link[data-label="' + CSS.escape(label) + '"]'); if (btn) btn.click(); return; } document.getElementById('ds-empty-state').classList.add('hidden'); var tabUuid = _newTabUuid(); var url = '/x/data_studio_database_overview/create?targetConn=' + encodeURIComponent(connName) + '&targetDb=' + encodeURIComponent(dbName) + '&tabLabel=' + encodeURIComponent(label) + '&tabUuid=' + encodeURIComponent(tabUuid); var content = document.createElement('div'); content.className = 'flex flex-col flex-1 overflow-hidden'; content.dataset.toolId = toolId; content.setAttribute('hx-get', url); content.setAttribute('hx-trigger', 'load'); content.setAttribute('hx-swap', 'innerHTML'); content.setAttribute('hx-indicator', '#content-loader'); content.setAttribute('hx-push-url', 'false'); var wsTab = document.getElementById('ds-workspace'); wsTab.addTab(label, content, true); htmx.process(content); _registerTab(toolId, label, { type: 'db_overview', label: label, tabLabel: label, tabUuid: tabUuid, toolId: toolId, connName: connName, dbName: dbName, url: url }); }; // ========== Copy-Collection Clipboard ========== var DS_CLIPBOARD_KEY = 'ds_clipboard'; window.dsGetClipboard = function() { try { return JSON.parse(sessionStorage.getItem(DS_CLIPBOARD_KEY) || 'null'); } catch (e) { return null; } }; window.dsSetClipboard = function(payload) { if (payload) sessionStorage.setItem(DS_CLIPBOARD_KEY, JSON.stringify(payload)); else sessionStorage.removeItem(DS_CLIPBOARD_KEY); }; window.dsCopyCollection = function(conn, db, coll) { window.dsSetClipboard({ type: 'copy-collection', srcConn: conn, srcDb: db, srcColl: coll }); wc.Prompt.toast({ title: 'Copied', text: conn + ' / ' + db + ' / ' + coll + ' — right-click a collection or database to paste', icon: 'info', timer: 2500 }); }; window.dsNormalizeFieldOrder = function(conn, db, coll) { var url = '/x/data_studio_normalize_field_order/create' + '?targetConn=' + encodeURIComponent(conn) + '&targetDb=' + encodeURIComponent(db) + '&collection=' + encodeURIComponent(coll); htmx.ajax('GET', url, { target: '#modal-container', swap: 'innerHTML', indicator: '#content-loader', pushUrl: false }); }; window.dsOpenPasteModal = function(tgtConn, tgtDb, tgtColl) { var cb = window.dsGetClipboard(); if (!cb || cb.type !== 'copy-collection') { wc.Prompt.toast({ title: 'Nothing to paste — right-click a collection and choose Copy first', icon: 'warning' }); return; } var url = '/x/data_studio_copy_collection/create' + '?srcConn=' + encodeURIComponent(cb.srcConn) + '&srcDb=' + encodeURIComponent(cb.srcDb) + '&srcColl=' + encodeURIComponent(cb.srcColl) + '&tgtConn=' + encodeURIComponent(tgtConn) + '&tgtDb=' + encodeURIComponent(tgtDb); if (tgtColl) url += '&tgtColl=' + encodeURIComponent(tgtColl); htmx.ajax('GET', url, { target: '#modal-container', swap: 'innerHTML', indicator: '#content-loader', pushUrl: false }); }; // Click a Save button by id, but only if it's currently visible (tab-safe). // Used by every Ctrl+S / Cmd+S hotkey in DataStudio's tab-embedded editors // so multi-tab workspaces don't all fire on one keypress. window.dsSaveByIdIfVisible = function(id) { var btn = document.getElementById(id); var modal = document.getElementById('modal-container'); var kids = modal ? modal.children.length : 'no container'; if (!btn) return; if (btn.offsetParent === null) return; if (modal && modal.children.length > 0) return; btn.click(); }; // ========== Init: bind workspace listeners + restore tabs ========== (function _initDataStudio() { var wsTab = document.getElementById('ds-workspace'); if (!wsTab) return; // Track active tab in URL hash + persist on every switch wsTab.addEventListener('wctabchange', function(e) { if (_RESTORING) return; // Inner wc-tab elements (Query/Parameters/Indexes/Schema, Results/ // Explain/Editor, etc.) bubble wctabchange up to this listener. Only // react when the event actually originated from the workspace tab — // otherwise a sub-tab click would clobber the URL hash. if (e.target.closest('wc-tab') !== wsTab) return; if (e.detail && e.detail.label) { var encoded = encodeURIComponent(e.detail.label); try { history.replaceState(null, '', '#' + encoded); } catch(_) { location.hash = encoded; } _persistTabs(); } // CodeMirror viewport fix: when a tab body was display:none and is // now re-shown, CM's internal char-width / gutter geometry is stale // and renders with the first column clipped until it refreshes. // Deferred via double-rAF so the layout is settled before we measure. requestAnimationFrame(function() { requestAnimationFrame(function() { var cms = wsTab.querySelectorAll('wc-code-mirror'); cms.forEach(function(cm) { if (cm.offsetParent === null) return; // still hidden — skip if (cm.editor && typeof cm.editor.refresh === 'function') { cm.editor.refresh(); } else if (typeof cm.display === 'function') { cm.display(); // wc-code-mirror fallback API } }); }); }); }); // dsGlobalWctabremoveHandler — single workspace-level cleanup. Per-tab // listeners went stale after dsRenameTab updated tab labels; this one // looks up state by current label at fire time, so it stays correct. wsTab.addEventListener('wctabremove', function(e) { if (!e.detail || !e.detail.label) return; var closedLabel = e.detail.label; // Find the openTabs entry whose live state.label matches the closed // tab's label (state.label is updated by dsRenameTab on save). for (var k in _openTabs) { var st = _openTabs[k]; if (st && st.label === closedLabel) { delete _openTabs[k]; break; } } checkEmptyState(); _persistTabs(); // Clear URL hash if no tabs remain. var remaining = wsTab.querySelectorAll(':scope > .wc-tab > .tab-nav > button.tab-link'); if (remaining.length === 0) { try { history.replaceState(null, '', location.pathname + location.search); } catch(_) {} } }); // Wait for wc-tab's internal .tab-nav/.tab-body to render before restoring. // wc-tab extends WcBaseComponent which exposes a `ready` Promise that // resolves after _render() creates the inner structure. if (wsTab.ready && typeof wsTab.ready.then === 'function') { wsTab.ready.then(_restoreTabs); } else { _restoreTabs(); } })(); studioHandleDeepLink({ test_data: function(qs) { window.dsOpenNewTool('test_data', 'Test Data', '/x/data_studio_test_data_gen/create'); }, script: function(qs) { window.dsOpenSavedScript(qs.get('id'), qs.get('slug') || ''); }, compare: function(qs) { window.dsOpenSavedCompare(qs.get('id'), qs.get('slug') || ''); }, visualQuery: function(qs) { window.dsOpenSavedVisualQuery(qs.get('id'), qs.get('slug') || ''); }, indexCompare: function(qs) { window.dsOpenSavedIndexCompare(qs.get('id'), qs.get('slug') || ''); }, import: function(qs) { window.dsOpenSavedImport(qs.get('id'), qs.get('slug') || ''); }, schemaDiff: function(qs) { window.dsOpenSavedSchemaDiff(qs.get('id'), qs.get('slug') || ''); }, reschema: function(qs) { window.dsOpenSavedReschema(qs.get('id'), qs.get('slug') || ''); }, masking: function(qs) { window.dsOpenSavedMasking(qs.get('id'), qs.get('slug') || ''); }, bulkCompare: function(qs) { window.dsOpenSavedBulkCompare(qs.get('id'), qs.get('slug') || ''); }, query: function(qs) { window.dsOpenSavedQuery( qs.get('conn') || '', qs.get('db') || '', qs.get('coll') || '', qs.get('id'), qs.get('type') || '', qs.get('slug') || '' ); }, lookups: function(qs) { window.dsOpenLookups(qs.get('target') || ''); }, collection: function(qs) { window.dsOpenCollection(qs.get('conn') || '', qs.get('db') || '', qs.get('coll') || ''); }, database: function(qs) { window.dsOpenDatabaseOverview(qs.get('conn') || '', qs.get('db') || ''); } });