import * as plugins from '../plugins.js'; import { apiService, changeStreamService, type ICollectionStats, type IMongoChangeEvent } from '../services/index.js'; import { formatSize, formatCount } from '../utilities/index.js'; import { themeStyles } from '../styles/index.js'; const { html, css, cssManager, customElement, property, state, DeesElement } = plugins; type TViewTab = 'documents' | 'indexes' | 'aggregation'; @customElement('tsview-mongo-browser') export class TsviewMongoBrowser extends DeesElement { @property({ type: String }) public accessor databaseName: string = ''; @property({ type: String }) public accessor collectionName: string = ''; @state() private accessor activeTab: TViewTab = 'documents'; @state() private accessor selectedDocumentId: string = ''; @state() private accessor stats: ICollectionStats | null = null; @state() private accessor editorWidth: number = 400; @state() private accessor isResizingEditor: boolean = false; @state() private accessor recentChangeCount: number = 0; @state() private accessor isStreamConnected: boolean = false; private changeSubscription: plugins.smartrx.rxjs.Subscription | null = null; public static styles = [ cssManager.defaultStyles, themeStyles, css` :host { display: block; height: 100%; } .browser-container { display: flex; flex-direction: column; height: 100%; } .header { display: flex; align-items: center; justify-content: space-between; padding: 12px; background: rgba(0, 0, 0, 0.2); border-radius: 8px; margin-bottom: 16px; } .collection-info { display: flex; align-items: center; gap: 16px; } .collection-title { font-size: 16px; font-weight: 500; } .collection-stats { display: flex; gap: 16px; font-size: 13px; color: #888; } .stat-item { display: flex; align-items: center; gap: 4px; } .tabs { display: flex; gap: 4px; } .tab { padding: 8px 16px; background: transparent; border: none; color: #888; cursor: pointer; font-size: 14px; border-radius: 6px; transition: all 0.15s; } .tab:hover { background: rgba(255, 255, 255, 0.05); color: #aaa; } .tab.active { background: rgba(255, 255, 255, 0.1); color: #e0e0e0; } .content { flex: 1; display: grid; grid-template-columns: 1fr 4px var(--editor-width, 400px); gap: 0; overflow: hidden; } .resize-divider { width: 4px; background: transparent; cursor: col-resize; transition: background 0.2s; } .resize-divider:hover, .resize-divider.active { background: rgba(255, 255, 255, 0.2); } .main-panel { overflow: auto; background: rgba(0, 0, 0, 0.2); border-radius: 8px; } .detail-panel { background: rgba(0, 0, 0, 0.2); border-radius: 8px; overflow: hidden; margin-left: 12px; } @media (max-width: 1200px) { .content { grid-template-columns: 1fr; } .detail-panel, .resize-divider { display: none; } } .change-indicator { display: flex; align-items: center; gap: 6px; padding: 4px 8px; background: rgba(34, 197, 94, 0.2); border-radius: 4px; font-size: 11px; color: #4ade80; } .change-indicator.pulse { animation: pulse-green 1s ease-in-out; } @keyframes pulse-green { 0% { background: rgba(34, 197, 94, 0.4); } 100% { background: rgba(34, 197, 94, 0.2); } } .stream-status { display: flex; align-items: center; gap: 4px; font-size: 11px; color: #888; } .stream-dot { width: 6px; height: 6px; border-radius: 50%; background: #888; } .stream-dot.connected { background: #22c55e; } `, ]; async connectedCallback() { super.connectedCallback(); await this.loadStats(); this.subscribeToChanges(); } disconnectedCallback() { super.disconnectedCallback(); this.unsubscribeFromChanges(); } updated(changedProperties: Map) { if (changedProperties.has('databaseName') || changedProperties.has('collectionName')) { this.loadStats(); this.selectedDocumentId = ''; this.recentChangeCount = 0; // Re-subscribe to the new collection this.unsubscribeFromChanges(); this.subscribeToChanges(); } } private async subscribeToChanges() { if (!this.databaseName || !this.collectionName) return; try { // Subscribe to collection changes const success = await changeStreamService.subscribeToCollection(this.databaseName, this.collectionName); this.isStreamConnected = success; if (success) { // Listen for changes this.changeSubscription = changeStreamService .getCollectionChanges(this.databaseName, this.collectionName) .subscribe((event) => { this.handleChange(event); }); } } catch (error) { console.warn('[MongoBrowser] Failed to subscribe to changes:', error); this.isStreamConnected = false; } } private unsubscribeFromChanges() { if (this.changeSubscription) { this.changeSubscription.unsubscribe(); this.changeSubscription = null; } if (this.databaseName && this.collectionName) { changeStreamService.unsubscribeFromCollection(this.databaseName, this.collectionName); } this.isStreamConnected = false; } private handleChange(event: IMongoChangeEvent) { console.log('[MongoBrowser] Received change:', event); this.recentChangeCount++; // Refresh stats to reflect changes this.loadStats(); // Notify the documents component to refresh const documentsEl = this.shadowRoot?.querySelector('tsview-mongo-documents') as any; if (documentsEl?.refresh) { documentsEl.refresh(); } } private async loadStats() { if (!this.databaseName || !this.collectionName) return; try { this.stats = await apiService.getCollectionStats(this.databaseName, this.collectionName); } catch (err) { console.error('Error loading stats:', err); this.stats = null; } } private setActiveTab(tab: TViewTab) { this.activeTab = tab; } private handleDocumentSelected(e: CustomEvent) { this.selectedDocumentId = e.detail.documentId; } private startEditorResize = (e: MouseEvent) => { e.preventDefault(); this.isResizingEditor = true; document.addEventListener('mousemove', this.handleEditorResize); document.addEventListener('mouseup', this.endEditorResize); }; private handleEditorResize = (e: MouseEvent) => { if (!this.isResizingEditor) return; const contentEl = this.shadowRoot?.querySelector('.content'); if (!contentEl) return; const containerRect = contentEl.getBoundingClientRect(); const newWidth = Math.min(Math.max(containerRect.right - e.clientX, 300), 700); this.editorWidth = newWidth; }; private endEditorResize = () => { this.isResizingEditor = false; document.removeEventListener('mousemove', this.handleEditorResize); document.removeEventListener('mouseup', this.endEditorResize); }; render() { return html`
${this.collectionName} ${this.stats ? html`
${formatCount(this.stats.count)} docs ${formatSize(this.stats.size)} ${this.stats.indexCount} indexes
` : ''}
${this.isStreamConnected ? 'Live' : 'Offline'}
${this.recentChangeCount > 0 ? html`
${this.recentChangeCount} change${this.recentChangeCount > 1 ? 's' : ''}
` : ''}
${this.activeTab === 'documents' ? html` ` : this.activeTab === 'indexes' ? html` ` : html`
Aggregation pipeline builder coming soon
`}
`; } }