Files
tsview/ts_web/elements/tsview-mongo-browser.ts

402 lines
11 KiB
TypeScript

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<string, unknown>) {
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`
<div class="browser-container">
<div class="header">
<div class="collection-info">
<span class="collection-title">${this.collectionName}</span>
${this.stats
? html`
<div class="collection-stats">
<span class="stat-item">${formatCount(this.stats.count)} docs</span>
<span class="stat-item">${formatSize(this.stats.size)}</span>
<span class="stat-item">${this.stats.indexCount} indexes</span>
</div>
`
: ''}
<div class="stream-status">
<span class="stream-dot ${this.isStreamConnected ? 'connected' : ''}"></span>
${this.isStreamConnected ? 'Live' : 'Offline'}
</div>
${this.recentChangeCount > 0
? html`
<div class="change-indicator pulse">
${this.recentChangeCount} change${this.recentChangeCount > 1 ? 's' : ''}
</div>
`
: ''}
</div>
<div class="tabs">
<button
class="tab ${this.activeTab === 'documents' ? 'active' : ''}"
@click=${() => this.setActiveTab('documents')}
>
Documents
</button>
<button
class="tab ${this.activeTab === 'indexes' ? 'active' : ''}"
@click=${() => this.setActiveTab('indexes')}
>
Indexes
</button>
<button
class="tab ${this.activeTab === 'aggregation' ? 'active' : ''}"
@click=${() => this.setActiveTab('aggregation')}
>
Aggregation
</button>
</div>
</div>
<div class="content" style="--editor-width: ${this.editorWidth}px">
<div class="main-panel">
${this.activeTab === 'documents'
? html`
<tsview-mongo-documents
.databaseName=${this.databaseName}
.collectionName=${this.collectionName}
@document-selected=${this.handleDocumentSelected}
></tsview-mongo-documents>
`
: this.activeTab === 'indexes'
? html`
<tsview-mongo-indexes
.databaseName=${this.databaseName}
.collectionName=${this.collectionName}
></tsview-mongo-indexes>
`
: html`
<div style="padding: 24px; text-align: center; color: #666;">
Aggregation pipeline builder coming soon
</div>
`}
</div>
<div
class="resize-divider ${this.isResizingEditor ? 'active' : ''}"
@mousedown=${this.startEditorResize}
></div>
<div class="detail-panel">
<tsview-mongo-document
.databaseName=${this.databaseName}
.collectionName=${this.collectionName}
.documentId=${this.selectedDocumentId}
></tsview-mongo-document>
</div>
</div>
</div>
`;
}
}