initial
This commit is contained in:
399
ts_web/elements/tsview-mongo-documents.ts
Normal file
399
ts_web/elements/tsview-mongo-documents.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { apiService } from '../services/index.js';
|
||||
|
||||
const { html, css, cssManager, customElement, property, state, DeesElement } = plugins;
|
||||
|
||||
@customElement('tsview-mongo-documents')
|
||||
export class TsviewMongoDocuments extends DeesElement {
|
||||
@property({ type: String })
|
||||
public accessor databaseName: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public accessor collectionName: string = '';
|
||||
|
||||
@state()
|
||||
private accessor documents: Record<string, unknown>[] = [];
|
||||
|
||||
@state()
|
||||
private accessor total: number = 0;
|
||||
|
||||
@state()
|
||||
private accessor page: number = 1;
|
||||
|
||||
@state()
|
||||
private accessor pageSize: number = 50;
|
||||
|
||||
@state()
|
||||
private accessor loading: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor filterText: string = '';
|
||||
|
||||
@state()
|
||||
private accessor selectedId: string = '';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.documents-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.filter-input:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
.filter-input::placeholder {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: 8px 16px;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border: 1px solid #6366f1;
|
||||
color: #818cf8;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background: rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.documents-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.document-row {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 4px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.document-row:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.document-row.selected {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.document-id {
|
||||
font-size: 12px;
|
||||
color: #818cf8;
|
||||
font-family: monospace;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.document-preview {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
font-family: monospace;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
border-top: 1px solid #333;
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid #444;
|
||||
color: #888;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.page-btn:hover:not(:disabled) {
|
||||
border-color: #666;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.actions-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 6px 12px;
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
border: 1px solid #22c55e;
|
||||
color: #4ade80;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await this.loadDocuments();
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('databaseName') || changedProperties.has('collectionName')) {
|
||||
this.page = 1;
|
||||
this.loadDocuments();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadDocuments() {
|
||||
if (!this.databaseName || !this.collectionName) return;
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
let filter = {};
|
||||
if (this.filterText.trim()) {
|
||||
try {
|
||||
filter = JSON.parse(this.filterText);
|
||||
} catch {
|
||||
// Invalid JSON, ignore filter
|
||||
}
|
||||
}
|
||||
|
||||
const result = await apiService.findDocuments(
|
||||
this.databaseName,
|
||||
this.collectionName,
|
||||
{
|
||||
filter,
|
||||
skip: (this.page - 1) * this.pageSize,
|
||||
limit: this.pageSize,
|
||||
}
|
||||
);
|
||||
|
||||
this.documents = result.documents;
|
||||
this.total = result.total;
|
||||
} catch (err) {
|
||||
console.error('Error loading documents:', err);
|
||||
this.documents = [];
|
||||
this.total = 0;
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
private handleFilterInput(e: Event) {
|
||||
this.filterText = (e.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private handleFilterSubmit() {
|
||||
this.page = 1;
|
||||
this.loadDocuments();
|
||||
}
|
||||
|
||||
private handleKeyPress(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
this.handleFilterSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
private selectDocument(doc: Record<string, unknown>) {
|
||||
const id = (doc._id as string) || '';
|
||||
this.selectedId = id;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('document-selected', {
|
||||
detail: { documentId: id, document: doc },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private goToPage(pageNum: number) {
|
||||
this.page = pageNum;
|
||||
this.loadDocuments();
|
||||
}
|
||||
|
||||
private getDocumentPreview(doc: Record<string, unknown>): string {
|
||||
const preview: Record<string, unknown> = {};
|
||||
const keys = Object.keys(doc).filter((k) => k !== '_id');
|
||||
for (const key of keys.slice(0, 3)) {
|
||||
preview[key] = doc[key];
|
||||
}
|
||||
return JSON.stringify(preview);
|
||||
}
|
||||
|
||||
private get totalPages(): number {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
}
|
||||
|
||||
private async handleInsertNew() {
|
||||
const newDoc = {
|
||||
// Default empty document
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
try {
|
||||
const insertedId = await apiService.insertDocument(
|
||||
this.databaseName,
|
||||
this.collectionName,
|
||||
newDoc
|
||||
);
|
||||
await this.loadDocuments();
|
||||
this.selectedId = insertedId;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('document-selected', {
|
||||
detail: { documentId: insertedId },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error inserting document:', err);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const startRecord = (this.page - 1) * this.pageSize + 1;
|
||||
const endRecord = Math.min(this.page * this.pageSize, this.total);
|
||||
|
||||
return html`
|
||||
<div class="documents-container">
|
||||
<div class="filter-bar">
|
||||
<input
|
||||
type="text"
|
||||
class="filter-input"
|
||||
placeholder='Filter: {"field": "value"}'
|
||||
.value=${this.filterText}
|
||||
@input=${this.handleFilterInput}
|
||||
@keypress=${this.handleKeyPress}
|
||||
/>
|
||||
<button class="filter-btn" @click=${this.handleFilterSubmit}>Apply</button>
|
||||
</div>
|
||||
|
||||
<div class="actions-bar">
|
||||
<button class="action-btn" @click=${this.handleInsertNew}>+ Insert Document</button>
|
||||
</div>
|
||||
|
||||
<div class="documents-list">
|
||||
${this.loading
|
||||
? html`<div class="loading-state">Loading...</div>`
|
||||
: this.documents.length === 0
|
||||
? html`<div class="empty-state">No documents found</div>`
|
||||
: this.documents.map(
|
||||
(doc) => html`
|
||||
<div
|
||||
class="document-row ${this.selectedId === doc._id ? 'selected' : ''}"
|
||||
@click=${() => this.selectDocument(doc)}
|
||||
>
|
||||
<div class="document-id">_id: ${doc._id}</div>
|
||||
<div class="document-preview">${this.getDocumentPreview(doc)}</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
||||
${this.total > 0
|
||||
? html`
|
||||
<div class="pagination">
|
||||
<div class="pagination-info">
|
||||
Showing ${startRecord}-${endRecord} of ${this.total}
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button
|
||||
class="page-btn"
|
||||
?disabled=${this.page <= 1}
|
||||
@click=${() => this.goToPage(this.page - 1)}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
class="page-btn"
|
||||
?disabled=${this.page >= this.totalPages}
|
||||
@click=${() => this.goToPage(this.page + 1)}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user