import * as plugins from '../plugins.js'; import { apiService, type IS3Object } from '../services/index.js'; import { formatSize, getFileName } from '../utilities/index.js'; import { themeStyles } from '../styles/index.js'; const { html, css, cssManager, customElement, property, state, DeesElement } = plugins; @customElement('tsview-s3-keys') export class TsviewS3Keys extends DeesElement { @property({ type: String }) public accessor bucketName: string = ''; @property({ type: String }) public accessor currentPrefix: string = ''; @property({ type: Number }) public accessor refreshKey: number = 0; @state() private accessor allKeys: IS3Object[] = []; @state() private accessor prefixes: string[] = []; @state() private accessor loading: boolean = false; @state() private accessor selectedKey: string = ''; @state() private accessor filterText: string = ''; public static styles = [ cssManager.defaultStyles, themeStyles, css` :host { display: block; height: 100%; overflow: hidden; } .keys-container { display: flex; flex-direction: column; height: 100%; } .filter-bar { padding: 12px; border-bottom: 1px solid #333; } .filter-input { width: 100%; padding: 8px 12px; background: rgba(0, 0, 0, 0.3); border: 1px solid #444; border-radius: 6px; color: #fff; font-size: 14px; } .filter-input:focus { outline: none; border-color: #404040; } .filter-input::placeholder { color: #666; } .keys-list { flex: 1; overflow-y: auto; } table { width: 100%; border-collapse: collapse; } thead { position: sticky; top: 0; background: #1a1a1a; z-index: 1; } th { text-align: left; padding: 10px 12px; font-size: 12px; font-weight: 500; color: #666; text-transform: uppercase; border-bottom: 1px solid #333; } td { padding: 8px 12px; font-size: 13px; border-bottom: 1px solid #2a2a3e; } tr:hover td { background: rgba(255, 255, 255, 0.03); } tr.selected td { background: rgba(255, 255, 255, 0.08); } .key-cell { display: flex; align-items: center; gap: 8px; cursor: pointer; } .key-icon { width: 16px; height: 16px; flex-shrink: 0; } .folder-icon { color: #fbbf24; } .key-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .size-cell { color: #888; font-variant-numeric: tabular-nums; } .empty-state { padding: 32px; text-align: center; color: #666; } `, ]; async connectedCallback() { super.connectedCallback(); await this.loadObjects(); } updated(changedProperties: Map) { if (changedProperties.has('bucketName') || changedProperties.has('currentPrefix') || changedProperties.has('refreshKey')) { this.loadObjects(); } } private async loadObjects() { this.loading = true; try { const result = await apiService.listObjects(this.bucketName, this.currentPrefix, '/'); this.allKeys = result.objects; this.prefixes = result.prefixes; } catch (err) { console.error('Error loading objects:', err); this.allKeys = []; this.prefixes = []; } this.loading = false; } private handleFilterInput(e: Event) { this.filterText = (e.target as HTMLInputElement).value; } private selectKey(key: string, isFolder: boolean) { this.selectedKey = key; if (isFolder) { this.dispatchEvent( new CustomEvent('navigate', { detail: { prefix: key }, bubbles: true, composed: true, }) ); } else { this.dispatchEvent( new CustomEvent('key-selected', { detail: { key }, bubbles: true, composed: true, }) ); } } private get filteredItems() { const filter = this.filterText.toLowerCase(); const folders = this.prefixes .filter((p) => !filter || getFileName(p).toLowerCase().includes(filter)) .map((p) => ({ key: p, isFolder: true, size: undefined })); const files = this.allKeys .filter((o) => !filter || getFileName(o.key).toLowerCase().includes(filter)) .map((o) => ({ key: o.key, isFolder: false, size: o.size })); return [...folders, ...files]; } render() { return html`
${this.loading ? html`
Loading...
` : this.filteredItems.length === 0 ? html`
No objects found
` : html` ${this.filteredItems.map( (item) => html` this.selectKey(item.key, item.isFolder)} > ` )}
Name Size
${item.isFolder ? html` ` : html` `} ${getFileName(item.key)}
${item.isFolder ? '-' : formatSize(item.size)}
`}
`; } }