import * as plugins from '../plugins.js'; import { apiService, changeStreamService, type IS3ChangeEvent } from '../services/index.js'; import { themeStyles } from '../styles/index.js'; const { html, css, cssManager, customElement, property, state, DeesElement } = plugins; type TViewType = 'columns' | 'keys'; @customElement('tsview-s3-browser') export class TsviewS3Browser extends DeesElement { @property({ type: String }) public accessor bucketName: string = ''; @state() private accessor viewType: TViewType = 'columns'; @state() private accessor currentPrefix: string = ''; @state() private accessor selectedKey: string = ''; @state() private accessor refreshKey: number = 0; @state() private accessor previewWidth: number = 350; @state() private accessor isResizingPreview: 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%; } .toolbar { display: flex; align-items: center; gap: 12px; padding: 12px; background: rgba(0, 0, 0, 0.2); border-radius: 8px; margin-bottom: 16px; } .breadcrumb { display: flex; align-items: center; gap: 4px; flex: 1; font-size: 14px; color: #999; } .breadcrumb-item { cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: background 0.15s; } .breadcrumb-item:hover { background: rgba(255, 255, 255, 0.1); color: #fff; } .breadcrumb-separator { color: #555; } .view-toggle { display: flex; gap: 4px; } .view-btn { padding: 6px 12px; background: transparent; border: 1px solid #444; color: #888; border-radius: 4px; cursor: pointer; font-size: 13px; transition: all 0.15s; } .view-btn:hover { border-color: #666; color: #aaa; } .view-btn.active { background: rgba(255, 255, 255, 0.1); border-color: #404040; color: #e0e0e0; } .content { flex: 1; display: grid; grid-template-columns: 1fr; gap: 0; overflow: hidden; } .content.has-preview { grid-template-columns: 1fr 4px var(--preview-width, 350px); } .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-view { overflow: auto; background: rgba(0, 0, 0, 0.2); border-radius: 8px; } .preview-panel { background: rgba(0, 0, 0, 0.2); border-radius: 8px; overflow: hidden; margin-left: 12px; } @media (max-width: 1024px) { .content, .content.has-preview { grid-template-columns: 1fr; } .preview-panel, .resize-divider { display: none; } } .stream-status { display: flex; align-items: center; gap: 4px; font-size: 11px; color: #888; margin-left: auto; margin-right: 12px; } .stream-dot { width: 6px; height: 6px; border-radius: 50%; background: #888; } .stream-dot.connected { background: #22c55e; } .change-indicator { display: flex; align-items: center; gap: 6px; padding: 4px 8px; background: rgba(245, 158, 11, 0.2); border-radius: 4px; font-size: 11px; color: #f59e0b; margin-right: 12px; } .change-indicator.pulse { animation: pulse-orange 1s ease-in-out; } @keyframes pulse-orange { 0% { background: rgba(245, 158, 11, 0.4); } 100% { background: rgba(245, 158, 11, 0.2); } } `, ]; async connectedCallback() { super.connectedCallback(); this.subscribeToChanges(); } disconnectedCallback() { super.disconnectedCallback(); this.unsubscribeFromChanges(); } private setViewType(type: TViewType) { this.viewType = type; } private navigateToPrefix(prefix: string) { this.currentPrefix = prefix; this.selectedKey = ''; } private handleKeySelected(e: CustomEvent) { this.selectedKey = e.detail.key; } private handleNavigate(e: CustomEvent) { this.navigateToPrefix(e.detail.prefix); } private handleObjectDeleted(e: CustomEvent) { this.selectedKey = ''; // Increment refresh key to trigger re-render of child components this.refreshKey++; } updated(changedProperties: Map) { if (changedProperties.has('bucketName')) { // Clear selection when bucket changes this.selectedKey = ''; this.currentPrefix = ''; this.recentChangeCount = 0; // Re-subscribe to the new bucket this.unsubscribeFromChanges(); this.subscribeToChanges(); } } private async subscribeToChanges() { if (!this.bucketName) return; try { // Subscribe to bucket changes (with optional prefix) const success = await changeStreamService.subscribeToBucket(this.bucketName, this.currentPrefix || undefined); this.isStreamConnected = success; if (success) { // Listen for changes this.changeSubscription = changeStreamService .getBucketChanges(this.bucketName, this.currentPrefix || undefined) .subscribe((event) => { this.handleChange(event); }); } } catch (error) { console.warn('[S3Browser] Failed to subscribe to changes:', error); this.isStreamConnected = false; } } private unsubscribeFromChanges() { if (this.changeSubscription) { this.changeSubscription.unsubscribe(); this.changeSubscription = null; } if (this.bucketName) { changeStreamService.unsubscribeFromBucket(this.bucketName, this.currentPrefix || undefined); } this.isStreamConnected = false; } private handleChange(event: IS3ChangeEvent) { console.log('[S3Browser] Received change:', event); this.recentChangeCount++; // Trigger refresh of child components this.refreshKey++; } private startPreviewResize = (e: MouseEvent) => { e.preventDefault(); this.isResizingPreview = true; document.addEventListener('mousemove', this.handlePreviewResize); document.addEventListener('mouseup', this.endPreviewResize); }; private handlePreviewResize = (e: MouseEvent) => { if (!this.isResizingPreview) return; const contentEl = this.shadowRoot?.querySelector('.content'); if (!contentEl) return; const containerRect = contentEl.getBoundingClientRect(); const newWidth = Math.min(Math.max(containerRect.right - e.clientX, 250), 600); this.previewWidth = newWidth; }; private endPreviewResize = () => { this.isResizingPreview = false; document.removeEventListener('mousemove', this.handlePreviewResize); document.removeEventListener('mouseup', this.endPreviewResize); }; render() { const breadcrumbParts = this.currentPrefix ? this.currentPrefix.split('/').filter(Boolean) : []; return html`
${this.isStreamConnected ? 'Live' : 'Offline'}
${this.recentChangeCount > 0 ? html`
${this.recentChangeCount} change${this.recentChangeCount > 1 ? 's' : ''}
` : ''}
${this.viewType === 'columns' ? html` ` : html` `}
${this.selectedKey ? html`
` : ''}
`; } }